Управление состоянием в React: подробный гайд
По мере роста React-приложения хранить все данные в одном компоненте становится неудобно: значения нужно передавать через десятки уровней и синхронизировать между элементами интерфейса. В этом уроке разберёмся, что такое управление состоянием, чем локальный state отличается от глобального, и какие инструменты — от Context API до внешних библиотек — помогают удержать порядок в данных приложения.
Что такое управление состоянием в 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 как способ управления состоянием
Как создать и использовать контекст
Контекст создаётся функцией 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» не существует — выбор зависит от масштаба состояния и характера его изменений. Перед тем как тянуть в проект библиотеку, стоит оценить несколько параметров:
- Сколько компонентов используют это состояние
- Как часто состояние обновляется
- Нужна ли синхронизация с сервером
- Важна ли отладка и история изменений
Хорошее правило — двигаться от простого к сложному: начинать с useState, поднимать состояние выше по дереву при необходимости, подключать Context, когда данные нужны в разных ветках дерева, и только при реальной нехватке этих инструментов подключать внешнюю библиотеку.
Прежде чем оборачивать всё приложение в Context, оберните передаваемое значение в useMemo. Без этого объект-значение будет создаваться заново при каждом рендере родителя, и все подписанные компоненты будут перерисовываться, даже если данные фактически не изменились.
Итоги
В этом уроке мы разобрались, зачем React-приложению нужно управление состоянием: от простого useState и useReducer для локальных данных до Context API и внешних библиотек для состояния, которое используется во многих частях приложения. Мы увидели, как избежать prop drilling и на какие критерии опираться при выборе подхода.
В следующем уроке подробно разберём Redux — самую известную библиотеку для управления состоянием в React.