← Назад к вопросам

Как реализовать batching операций с иммутабельным состоянием в React?

2.0 Middle🔥 211 комментариев
#React#Архитектура и паттерны

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как реализовать batching операций с иммутабельным состоянием в React

Batching - это техника объединения нескольких обновлений состояния в один рендер. Это улучшает производительность, так как избегает множественных ре-рендеров. Вместе с иммутабельным состоянием это становится мощной комбинацией.

Автоматический Batching в React 18+

React 18 автоматически батчит обновления в синхронных событиях:

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  const handleClick = () => {
    // React автоматически батчит оба обновления в один рендер
    setCount(count + 1);
    setName('Updated');
    // Только ОДИН рендер, а не два!
  };

  console.log('Render');
  return (
    <div>
      Count: {count}, Name: {name}
      <button onClick={handleClick}>Update Both</button>
    </div>
  );
}

Однако асинхронные операции требуют явного батчинга.

Батчинг с useReducer (рекомендуется)

В большинстве случаев useReducer - лучший подход для батчинга:

interface State {
  count: number;
  name: string;
  isLoading: boolean;
  error: string | null;
}

type Action = 
  | { type: 'INCREMENT' }
  | { type: 'SET_NAME'; payload: string }
  | { type: 'SET_LOADING'; payload: boolean }
  | { type: 'BATCH'; payload: Action[] };

const initialState: State = {
  count: 0,
  name: '',
  isLoading: false,
  error: null,
};

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };
    
    case 'SET_NAME':
      return { ...state, name: action.payload };
    
    case 'SET_LOADING':
      return { ...state, isLoading: action.payload };
    
    case 'BATCH': {
      // Применяем все действия последовательно
      return action.payload.reduce(reducer, state);
    }
    
    default:
      return state;
  }
}

function Component() {
  const [state, dispatch] = useReducer(reducer, initialState);

  const handleLoadUser = async (userId: string) => {
    // Батчим несколько действий в одно
    dispatch({ type: 'BATCH', payload: [
      { type: 'SET_LOADING', payload: true },
      { type: 'SET_NAME', payload: '' },
    ]});

    try {
      const response = await fetch(`/api/users/${userId}`);
      const user = await response.json();
      
      // Батчим результат
      dispatch({ type: 'BATCH', payload: [
        { type: 'SET_NAME', payload: user.name },
        { type: 'SET_LOADING', payload: false },
      ]});
    } catch (error) {
      dispatch({ type: 'SET_LOADING', payload: false });
    }
  };

  return (
    <div>
      <p>Count: {state.count}</p>
      <p>Name: {state.name}</p>
      {state.isLoading && <p>Loading...</p>}
      <button onClick={() => handleLoadUser('1')}>Load User</button>
    </div>
  );
}

Батчинг с useState и flushSync

Для более сложных сценариев используй flushSync из React:

import { useState, useTransition } from 'react';
import { flushSync } from 'react-dom';

function App() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  const [isPending, startTransition] = useTransition();

  const handleBatchUpdate = () => {
    // startTransition - батчит обновления в один рендер
    startTransition(async () => {
      setCount(c => c + 1);
      setName('Updated');
      // Один рендер для обоих обновлений
    });
  };

  const handleUrgentUpdate = () => {
    // flushSync - выполняет обновление сразу (отключает батчинг)
    flushSync(() => {
      setCount(c => c + 1);
    });
    // Рендер происходит СРАЗУ, не батчится
    
    // Потом можно батчить остальное
    setName('Updated');
  };

  return (
    <>
      <p>Count: {count}, Name: {name}</p>
      <button onClick={handleBatchUpdate}>Batch Update</button>
      <button onClick={handleUrgentUpdate}>Urgent Update</button>
      {isPending && <p>Pending...</p>}
    </>
  );
}

Иммутабельное состояние с батчингом

Для сложных объектов используй вспомогательные функции:

interface UserProfile {
  id: string;
  name: string;
  email: string;
  settings: {
    theme: 'light' | 'dark';
    notifications: boolean;
  };
}

type UpdateAction = 
  | { type: 'SET_NAME'; payload: string }
  | { type: 'SET_EMAIL'; payload: string }
  | { type: 'SET_THEME'; payload: 'light' | 'dark' }
  | { type: 'BATCH'; payload: UpdateAction[] };

function userReducer(state: UserProfile, action: UpdateAction): UserProfile {
  switch (action.type) {
    case 'SET_NAME':
      return { ...state, name: action.payload };
    
    case 'SET_EMAIL':
      return { ...state, email: action.payload };
    
    case 'SET_THEME':
      return {
        ...state,
        settings: {
          ...state.settings,
          theme: action.payload,
        },
      };
    
    case 'BATCH': {
      return action.payload.reduce(userReducer, state);
    }
    
    default:
      return state;
  }
}

function UserEditor() {
  const [user, dispatch] = useReducer(userReducer, initialUser);

  const handleFormSubmit = (formData: any) => {
    // Батчим все обновления профиля
    dispatch({
      type: 'BATCH',
      payload: [
        { type: 'SET_NAME', payload: formData.name },
        { type: 'SET_EMAIL', payload: formData.email },
        { type: 'SET_THEME', payload: formData.theme },
      ],
    });
  };

  return <div>User: {user.name}</div>;
}

Батчинг с Immer (популярная библиотека)

Immer упрощает работу с иммутабельным состоянием:

import produce from 'immer';
import { useState } from 'react';

interface State {
  users: Array<{ id: string; name: string; active: boolean }>;
  filter: string;
}

function App() {
  const [state, setState] = useState<State>({
    users: [],
    filter: '',
  });

  // С Immer можно писать мутирующий код, но получаешь иммутабельное состояние
  const batchUpdateUsers = (updates: Array<{ id: string; name: string }>) => {
    setState(
      produce((draft) => {
        // Пишем как будто мутируем
        for (const update of updates) {
          const user = draft.users.find(u => u.id === update.id);
          if (user) {
            user.name = update.name;
          }
        }
        draft.filter = '';
      })
    );
    // Но state остается иммутабельным благодаря Immer
  };

  return <div>App</div>;
}

Батчинг с Zustand (современный state management)

Zustand поддерживает батчинг из коробки:

import create from 'zustand';

interface Store {
  count: number;
  name: string;
  theme: string;
  batchUpdate: (updates: Partial<Store>) => void;
}

const useStore = create<Store>((set) => ({
  count: 0,
  name: '',
  theme: 'light',
  batchUpdate: (updates) => {
    set(updates);
    // Zustand автоматически батчит обновления
  },
}));

function App() {
  const { count, name, batchUpdate } = useStore();

  const handleUpdate = () => {
    // Один рендер для всех обновлений
    batchUpdate({
      count: count + 1,
      name: 'Updated',
      theme: 'dark',
    });
  };

  return (
    <>
      <p>Count: {count}, Name: {name}</p>
      <button onClick={handleUpdate}>Batch Update</button>
    </>
  );
}

Практический пример: Todo app с батчингом

interface Todo {
  id: string;
  text: string;
  completed: boolean;
}

type TodoAction =
  | { type: 'ADD_TODO'; payload: string }
  | { type: 'TOGGLE_TODO'; payload: string }
  | { type: 'REMOVE_TODO'; payload: string }
  | { type: 'BATCH_OPERATIONS'; payload: TodoAction[] };

function todoReducer(todos: Todo[], action: TodoAction): Todo[] {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...todos,
        { id: Date.now().toString(), text: action.payload, completed: false },
      ];
    
    case 'TOGGLE_TODO':
      return todos.map(t =>
        t.id === action.payload ? { ...t, completed: !t.completed } : t
      );
    
    case 'REMOVE_TODO':
      return todos.filter(t => t.id !== action.payload);
    
    case 'BATCH_OPERATIONS':
      return action.payload.reduce(todoReducer, todos);
    
    default:
      return todos;
  }
}

function TodoApp() {
  const [todos, dispatch] = useReducer(todoReducer, []);

  const handleBulkToggleCompleted = (ids: string[]) => {
    // Батчим все toggle операции
    dispatch({
      type: 'BATCH_OPERATIONS',
      payload: ids.map(id => ({
        type: 'TOGGLE_TODO' as const,
        payload: id,
      })),
    });
  };

  const handleSync = async () => {
    const response = await fetch('/api/todos');
    const newTodos = await response.json();
    
    // Батчим все операции
    const operations: TodoAction[] = [
      ...todos.map(t => ({ type: 'REMOVE_TODO' as const, payload: t.id })),
      ...newTodos.map(t => ({ type: 'ADD_TODO' as const, payload: t.text })),
    ];
    
    dispatch({ type: 'BATCH_OPERATIONS', payload: operations });
  };

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

Лучшие практики батчинга

  1. Используй useReducer для батчинга - это самый явный способ
  2. Избегай flushSync - батчинг обычно лучше
  3. Используй startTransition для больших обновлений
  4. Рассмотри Zustand или Jotai для простого состояния
  5. Используй Immer для сложного иммутабельного состояния
  6. Профилируй производительность - батчинг должен улучшить метрики

Батчинг с иммутабельным состоянием - это ключ к высокопроизводительным React приложениям.

Как реализовать batching операций с иммутабельным состоянием в React? | PrepBro