Хотите заказать веб-сайт? Связаться с нами

Управление состоянием в React: подробный гайд

По мере роста React-приложения хранить все данные в одном компоненте становится неудобно: значения нужно передавать через десятки уровней и синхронизировать между элементами интерфейса. В этом уроке разберёмся, что такое управление состоянием, чем локальный state отличается от глобального, и какие инструменты — от Context API до внешних библиотек — помогают удержать порядок в данных приложения.

Управление состоянием в React: подробный гайд

Что такое управление состоянием в React и зачем оно нужно

Состояние (state) — это данные, которые могут изменяться со временем и от которых зависит то, что видит пользователь. Как только значение состояния меняется, React перерисовывает компонент и все его дочерние элементы, которые от этого значения зависят.

Управление состоянием — это про то, где хранить эти данные, кто имеет право их менять и как эти изменения доходят до нужных частей интерфейса.

Пока приложение маленькое, с этим справляется один-два вызова useState внутри компонента. Но когда компонентов становится больше, а данные нужны в разных, часто далёких друг от друга частях дерева, наивный подход начинает давать сбои: одни и те же данные дублируются, обновления теряются, а логика изменения состояния расползается по десяткам файлов. Именно поэтому управление состоянием выделяют в отдельную задачу архитектуры приложения, а не считают мелкой деталью реализации.

Локальное состояние компонента

Самый простой вид состояния — локальное состояние, которое живёт внутри одного компонента и не интересует остальное приложение. Для большинства случаев его вполне достаточно.

useState для простых случаев

Хук useState отлично подходит, когда состояние — это одно независимое значение: открыт ли попап, выбран ли пункт меню, что введено в поле формы.

import { useState } from 'react';
 
function Counter() {
  const [count, setCount] = useState(0);
 
  return (
    <button onClick={() => setCount(count + 1)}>
      Кликов: {count}
    </button>
  );
}

useReducer для сложной логики

Когда состояние превращается в объект с несколькими связанными полями, а переходы между значениями подчиняются определённым правилам, вместо нескольких useState удобнее взять useReducer. Он собирает всю логику изменений в одном месте — функции reducer, — и компонент только отправляет действия (actions), не заботясь о деталях.

import { useReducer } from 'react';
 
type State = { count: number };
type Action = { type: 'increment' } | { type: 'decrement' };
 
function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      return state;
  }
}
 
function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
 
  return (
    <div>
      <span>{state.count}</span>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </div>
  );
}

Если ловите себя на том, что обновляете сразу несколько useState в одном обработчике события — это сигнал, что поля состояния связаны между собой и их стоит объединить в один useReducer. Так вы не забудете обновить связанное значение и получите единую точку правды для всей логики.

Проблема prop drilling

Как только состояние нужно не только тому компоненту, где оно объявлено, но и его дальним потомкам, возникает соблазн просто передать значение через props на каждом уровне дерева. Это и называют prop drilling — состояние «просачивается» вниз через компоненты, которым оно самим не нужно, а нужно только чтобы передать его дальше.

function App() {
  const [user, setUser] = useState({ name: 'Аня' });
 
  return <Page user={user} />;
}
 
function Page({ user }) {
  return <Sidebar user={user} />;
}
 
function Sidebar({ user }) {
  return <UserInfo user={user} />;
}
 
function UserInfo({ user }) {
  return <p>Привет, {user.name}!</p>;
}

В примере выше компоненты Page и Sidebar ничего не делают с user — они просто прокидывают его дальше. На небольшом дереве это терпимо, но при росте приложения такие цепочки становятся длинными, а любое изменение структуры компонентов ломает передачу данных.

Context API как способ управления состоянием

Context API — встроенный в React механизм, который позволяет передать данные напрямую от компонента-источника к любому потомку, минуя промежуточные уровни.

Как создать и использовать контекст

Контекст создаётся функцией createContext, оборачивается в компонент Provider, а читается через хук useContext.

import { createContext, useContext, useState } from 'react';
 
const UserContext = createContext(null);
 
function App() {
  const [user, setUser] = useState({ name: 'Аня' });
 
  return (
    <UserContext.Provider value={user}>
      <Page />
    </UserContext.Provider>
  );
}
 
function UserInfo() {
  const user = useContext(UserContext);
  return <p>Привет, {user.name}!</p>;
}

Ограничения Context API

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

Внешние библиотеки для управления состоянием

Когда состояние приложения становится действительно сложным — с асинхронной загрузкой данных, отменой действий, синхронизацией между вкладками — на сцену выходят специализированные библиотеки.

Redux

(откроется в новой вкладке)Redux хранит всё состояние приложения в одном объекте — store, а изменения происходят только через action и чистые функции reducer. Такой строгий и предсказуемый поток данных упрощает отладку: в DevTools видна вся история изменений состояния. Подробно с Redux мы познакомимся в следующем уроке.

MobX

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

Современные лёгкие решения

Помимо Redux и MobX, популярность набрали более лёгкие библиотеки — Zustand, Jotai, Recoil. Они убирают часть шаблонного кода классического Redux, сохраняя предсказуемый поток данных, и часто становятся хорошим компромиссом для проектов среднего размера.

Подход Когда использовать
useState Простое независимое состояние: счётчики, чекбоксы, значения полей формы
useReducer Сложная логика с несколькими взаимосвязанными полями и переходами состояний
Context API Данные, редко меняющиеся, но нужные многим компонентам: тема, язык, авторизованный пользователь
Redux / MobX / Zustand Большое приложение с частыми обновлениями, сложной бизнес-логикой и потребностью в отладке (DevTools)

Как выбрать подход для своего проекта

Универсального ответа «всегда используйте X» не существует — выбор зависит от масштаба состояния и характера его изменений. Перед тем как тянуть в проект библиотеку, стоит оценить несколько параметров:

  1. Сколько компонентов используют это состояние
  2. Как часто состояние обновляется
  3. Нужна ли синхронизация с сервером
  4. Важна ли отладка и история изменений

Хорошее правило — двигаться от простого к сложному: начинать с useState, поднимать состояние выше по дереву при необходимости, подключать Context, когда данные нужны в разных ветках дерева, и только при реальной нехватке этих инструментов подключать внешнюю библиотеку.

Прежде чем оборачивать всё приложение в Context, оберните передаваемое значение в useMemo. Без этого объект-значение будет создаваться заново при каждом рендере родителя, и все подписанные компоненты будут перерисовываться, даже если данные фактически не изменились.

Итоги

В этом уроке мы разобрались, зачем React-приложению нужно управление состоянием: от простого useState и useReducer для локальных данных до Context API и внешних библиотек для состояния, которое используется во многих частях приложения. Мы увидели, как избежать prop drilling и на какие критерии опираться при выборе подхода.

В следующем уроке подробно разберём Redux — самую известную библиотеку для управления состоянием в React.

Теги: