Назад
Назад к заметкам
· 4 мин чтения

React управление состоянием: синхронизация формы и таблицы

Легкая и масштабируемая схема React управление состоянием: синхронизация формы и таблицы через useReducer и Context.

reactstate-managementuseReducercontextperformance

React управление состоянием часто становится узким местом, когда ввод в форму появляется в ячейках таблицы с задержкой. Причина обычно разбросанный стейт между компонентами. Держать отдельный useState в каждом элементе быстро приводит к множеству ререндеров и “тряскам” UI. Давайте посмотрим, как собрать легкую, но масштабируемую схему, чтобы форма и таблица оставались в полном согласовании.

В конце вы получите готовый шаблон с useReducer и Context, а также чеклист типичных ошибок. Решение пригодится сразу после того, как в проекте появятся взаимосвязанные формы и списки.

Централизованное состояние: когда React управление состоянием действительно нужно

Ставить каждый кусок данных в отдельный useState удобно, но быстро становится дорого, когда:

  • несколько компонентов читают и изменяют один и тот же кусок данных;
  • требуется сложная логика обновления (например, валидация + автосохранение);
  • нужно отследить изменения для отладки.

В таких случаях лучше собрать централизованное состояние. Оно выглядит как один источник правды, откуда все “потомки” берут данные и куда отправляют изменения.

useReducer vs useState: где уместно использовать каждый

ФакторuseStateuseReducer
ПростотаДа (один кусок данных)Нет (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 или других библиотек управления состоянием.

Готов начать?

Выбери удобный способ связи — напиши напрямую или оставь заявку

Написать в Telegram

Отвечу в течение пары часов