React управление состоянием: синхронизация формы и таблицы
Легкая и масштабируемая схема React управление состоянием: синхронизация формы и таблицы через useReducer и Context.
React управление состоянием часто становится узким местом, когда ввод в форму появляется в ячейках таблицы с задержкой. Причина обычно разбросанный стейт между компонентами. Держать отдельный useState
в каждом элементе быстро приводит к множеству ререндеров и “тряскам” UI. Давайте посмотрим, как собрать легкую, но масштабируемую схему, чтобы форма и таблица оставались в полном согласовании.
В конце вы получите готовый шаблон с useReducer
и Context, а также чеклист типичных ошибок. Решение пригодится сразу после того, как в проекте появятся взаимосвязанные формы и списки.
Централизованное состояние: когда React управление состоянием действительно нужно
Ставить каждый кусок данных в отдельный useState
удобно, но быстро становится дорого, когда:
- несколько компонентов читают и изменяют один и тот же кусок данных;
- требуется сложная логика обновления (например, валидация + автосохранение);
- нужно отследить изменения для отладки.
В таких случаях лучше собрать централизованное состояние. Оно выглядит как один источник правды, откуда все “потомки” берут данные и куда отправляют изменения.
useReducer vs useState: где уместно использовать каждый
Фактор | useState | useReducer |
---|---|---|
Простота | Да (один кусок данных) | Нет (requires action & reducer) |
Сложная логика | Плохо (множество setState вызовов) | Отлично (единственная функция-редуктор) |
Трассировка изменений | Ограничена | Полна (action.type, payload) |
Если состояние представляет собой простой скаляр оставьте useState
. Когда же требуется обработка нескольких полей формы, валидация и синхронизация включайте useReducer
.
import React, { useReducer } from "react";
type State = { name: string; age: string };
type Action =
| { type: "SET_NAME"; payload: string }
| { type: "SET_AGE"; payload: string };
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "SET_NAME":
return { ...state, name: action.payload };
case "SET_AGE":
return { ...state, age: action.payload };
default:
return state;
}
};
export const Form = () => {
const [state, dispatch] = useReducer(reducer, { name: "", age: "" });
return (
<div>
<input
value={state.name}
onChange={(e) =>
dispatch({ type: "SET_NAME", payload: e.target.value })
}
placeholder="Имя"
/>
<input
value={state.age}
onChange={(e) => dispatch({ type: "SET_AGE", payload: e.target.value })}
placeholder="Возраст"
/>
</div>
);
};
Эта схема гарантирует, что любые изменения проходят через один редуктор, а значит легко отследить их в DevTools.
Контекст API: доставляем состояние в глубину дерева
useReducer
уже дает единый стейт, но как “подтолкнуть” его в дочерние компоненты? Ответ React.createContext
. Используем контекст, чтобы реализовать React управление состоянием на уровне приложения.
import React, { createContext, useContext, ReactNode, useReducer } from "react";
const FormContext = createContext<{
state: State;
dispatch: React.Dispatch<Action>;
} | null>(null);
export const FormProvider = ({ children }: { children: ReactNode }) => {
const [state, dispatch] = useReducer(reducer, { name: "", age: "" });
return (
<FormContext.Provider value={{ state, dispatch }}>
{children}
</FormContext.Provider>
);
};
export const useForm = () => {
const ctx = useContext(FormContext);
if (!ctx) throw new Error("useForm must be used within FormProvider");
return ctx;
};
В любой вложенной таблице достаточно вызвать useForm()
и получить актуальные данные без проброса пропсов через несколько уровней. Такой подход особенно полезен при работе с большими формами, где каждый ряд таблицы отдельный компонент.
Мемоизация и избежание лишних ререндеров
Когда контекст меняет состояние, все подписанные компоненты перерисовываются. Чтобы сократить количество лишних рендеров, используйте React.memo
и useMemo
. Подробнее о мемоизации компонентов можно прочитать в статье мемоизация компонентов.
import React, { memo } from "react";
import { useForm } from "./FormProvider";
const Row = memo(({ index }: { index: number }) => {
const { state } = useForm();
return (
<tr>
<td>{index + 1}</td>
<td>{state.name}</td>
<td>{state.age}</td>
</tr>
);
});
export const Table = () => {
const rows = Array.from({ length: 10 });
return (
<table>
<tbody>
{rows.map((_, i) => (
<Row key={i} index={i} />
))}
</tbody>
</table>
);
};
Row
будет перерисовываться только тогда, когда изменятся state.name
или state.age
. Если же какие-то поля не задействованы в конкретной ячейке, обновления их не коснутся. Для более глобального улучшения рендеринга обратите внимание на оптимизацию рендера.
Практический чеклист по управлению состоянием
- Выбирайте
useReducer
, если логика обновления сложнее простого присваивания. - Оборачивайте стейт в Context, чтобы избежать “проброса” пропсов через несколько уровней.
- Мемоизируйте листовые компоненты (
React.memo
) и вычисляемые данные (useMemo
). - Не храните в стейте derived-данные вычисляйте их на лету или через
useMemo
. - Следите за “захламлением” reducer: если действия становятся слишком специфичными, разбейте их на несколько небольших редукторов.
Эти пункты спасут от большинства типовых проблем, когда форма и таблица начинают “дергаться” при каждом вводе.
С такой схемой ваш UI будет реагировать мгновенно, а код останется чистым и масштабируемым.
При появлении новых полей формы просто расширяйте reducer и Context остальные компоненты уже готовы к работе.
Для больших приложений рассмотрите использование Redux или других библиотек управления состоянием.