← Назад к вопросам
Как реализовать 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>
);
}
Лучшие практики батчинга
- Используй useReducer для батчинга - это самый явный способ
- Избегай flushSync - батчинг обычно лучше
- Используй startTransition для больших обновлений
- Рассмотри Zustand или Jotai для простого состояния
- Используй Immer для сложного иммутабельного состояния
- Профилируй производительность - батчинг должен улучшить метрики
Батчинг с иммутабельным состоянием - это ключ к высокопроизводительным React приложениям.