← назад к разделу

Состояние — это данные, которые меняются в процессе работы приложения и от которых зависит то, что видит пользователь. Открыт ли модал, что введено в поле, список товаров с сервера — всё это состояние. Разобраться, где хранить каждый вид, — первый шаг к тому, чтобы код не превратился в клубок.

Локальное состояниеспросят на собеседовании

Большая часть состояния — локальная: открыт ли выпадающий список, что введено в поле, какая вкладка активна. Такие данные важны только одному компоненту. Инструмент для этого — useState.

function SearchBox() {
  const [query, setQuery] = useState("");
  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

Правило простое: держи состояние как можно ближе к тому месту, где оно используется. Не нужно поднимать его выше без причины.

Когда переходов много и они связаны между собой (например, многошаговая форма), вместо россыпи useState берут useReducer. Он собирает все переходы в одном месте:

type Action = { type: "next" } | { type: "prev" } | { type: "reset" };

function reducer(step: number, action: Action): number {
  switch (action.type) {
    case "next": return step + 1;
    case "prev": return step - 1;
    case "reset": return 0;
  }
}

function Wizard() {
  const [step, dispatch] = useReducer(reducer, 0);
  return (
    <div>
      <p>Шаг {step}</p>
      <button onClick={() => dispatch({ type: "next" })}>Далее</button>
    </div>
  );
}

useReducer удобен, когда следующее значение зависит от предыдущего и переходов больше двух-трёх.

Разделяемое состояние и contextспросят на собеседовании

Бывает, что одно и то же состояние нужно нескольким компонентам. Сначала попробуй поднять его к общему родителю — это самый простой способ. Если родитель далеко и пробрасывать через много уровней неудобно, используют context.

const ThemeContext = createContext<"light" | "dark">("light");

function App() {
  const [theme, setTheme] = useState<"light" | "dark">("light");
  return (
    <ThemeContext.Provider value={theme}>
      <Page />
    </ThemeContext.Provider>
  );
}

function Button() {
  const theme = useContext(ThemeContext); // получаем тему без пробрасывания props
  return <button className={theme}>Нажми</button>;
}

Важная особенность: при изменении значения context перерисовываются все компоненты, которые его читают. Поэтому context хорошо работает для редко меняющихся данных — тема оформления, текущий пользователь, язык интерфейса. Для часто меняющихся данных (позиция мыши, текст поля) он вызовет лишние перерисовки.

Два слова, которые стоит знать по именам. Проброс пропсов через несколько уровней, где промежуточные компоненты только передают их дальше, называют prop drilling, и контекст это его лекарство. Но у контекста есть цена: при смене значения перерисовываются все компоненты, которые его читают, даже если им нужно одно поле из десяти, потому что подписка идёт на значение целиком. Внешние сторы вроде Zustand или Redux подписывают компонент на выбранный кусок через селектор и перерисовывают только тех, у кого этот кусок изменился. Поэтому контекст хорош для редко меняющихся вещей, темы, пользователя, локали, а для часто меняющихся данных берут стор или делят контекст на несколько маленьких.

Серверное состояние — это другоеспросят на собеседовании

Вот ключевое различие, которое упрощает многое: данные с сервера — это не состояние приложения, это временная копия серверных данных на клиенте.

Список товаров, профиль пользователя, история заказов — всё это живёт на сервере. На клиенте у тебя лишь копия, которую нужно:

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

Если хранить серверные данные в useState, всё это придётся писать вручную. А это уже давно решённая задача: для серверного состояния есть TanStack Query. Он берёт на себя кеширование, повторные запросы, гонки запросов и инвалидацию.

function ProductList() {
  const { data, isPending, isError } = useQuery({
    queryKey: ["products"],
    queryFn: () => fetch("/api/products").then((r) => r.json()),
  });

  if (isPending) return <p>Загрузка...</p>;
  if (isError) return <p>Ошибка загрузки</p>;
  return <ul>{data.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}

Большая часть того, что новички кладут в глобальный стор, — это серверные данные. После того как они уходят в TanStack Query, глобального состояния остаётся совсем мало.

Когда нужен внешний сторспросят на собеседовании

После того как серверное состояние ушло в TanStack Query, а локальное — в useState, для глобального стора (Zustand, Redux) остаётся немного:

  • содержимое корзины до отправки на сервер — это клиентское состояние, которое нужно в разных частях приложения;
  • состояние сложного мастера на несколько экранов — когда данные из шага 1 нужны на шаге 4;
  • глобальные UI-флаги — например, открыта ли боковая панель.

Критерий: стор оправдан, если состояние одновременно (1) клиентское, не кеш сервера, (2) нужно несвязанным компонентам в разных частях дерева и (3) часто меняется, поэтому context не подходит. Если хоть одно условие не выполняется — скорее всего, стор преждевременен.

Zustand — простой выбор для начала: минимум кода, нет boilerplate.

import { create } from "zustand";

interface CartStore {
  items: string[];
  add: (item: string) => void;
}

const useCart = create<CartStore>((set) => ({
  items: [],
  add: (item) => set((state) => ({ items: [...state.items, item] })),
}));

function AddButton({ name }: { name: string }) {
  const add = useCart((s) => s.add);
  return <button onClick={() => add(name)}>Добавить</button>;
}

Как выбрать инструмент

Простая схема принятия решения:

  1. Состояние нужно только одному компоненту → useState или useReducer.
  2. Нужно нескольким компонентам, но меняется редко → context.
  3. Это данные с сервера → TanStack Query.
  4. Клиентское, нужно многим, часто меняется → стор (Zustand или Redux).
состояние useState или useReducer нужно одному компоненту context нескольким, меняется редко TanStack Query пришло с сервера стор: Zustand, Redux клиентское, многим, часто меняется

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

Коротко

  • Состояние — данные, от которых зависит то, что видит пользователь.
  • Держи состояние как можно ближе к месту использования: начинай с useState.
  • useReducer — когда переходов много и они связаны между собой.
  • context — для редко меняющихся данных, которые нужны глубоко в дереве (тема, пользователь, локаль).
  • Серверные данные — это кеш, не состояние приложения. Для них — TanStack Query.
  • Глобальный стор (Zustand, Redux) оправдан только для клиентского состояния, которое нужно несвязанным компонентам и часто меняется.

Что почитать дальше