Локальный state в React: когда и почему

Кратко: храните состояние там, где оно используется. Локальный state упрощает компоненты, уменьшает количество props и снижает когнитивную нагрузку. Когда несколько компонентов действительно разделяют данные — тогда поднимайте состояние вверх или используйте Context/Redux.
В этой статье объясняю, почему часто ошибочно хранят состояние в родительских компонентах, когда оно нужно только дочернему, и как рефакторить код, чтобы сделать состояние локальным. Приведены практические примеры, альтернативы, чек-листы для миграции и схема принятия решения.
Зачем делать состояние локальным
Коротко:
- Локальное состояние уменьшает поверхность изменений: изменение состояния затрагивает только один компонент.
- Компоненты становятся более автономными и переиспользуемыми.
- Меньше передачи props и меньше «протаскивания» функций setX через дерево компонентов.
Важно: локальный state — не универсальное решение. Если несколько независимых веток дерева нуждаются в одних и тех же данных, логичнее поднять состояние до ближайшего общего предка или использовать глобальный стор.
Базовый пример: счётчик
Ниже — исходный (типичный) пример, где состояние создаётся в App и прокидывается в Counter через props:
import {useState} from 'react'
import {Counter} from 'counter'
function App(){
const [count, setCount] = useState(0)
return
}
export default App
Компонент Counter использует count и setCount:
function Counter({count, setCount}) {
return (
{count}
)
}
Проблема: в этом примере только Counter нуждается в count. Мы подняли состояние на уровень выше без необходимости — это увеличивает связность и усложняет рефакторинг.
Перемещаем состояние в дочерний компонент
Лучше объявить state прямо в Counter:
import {useState} from 'react'
function Counter() {
const [count, setCount] = useState(0)
return (
{count}
)
}
Тогда App упрощается:
// imports
function App(){
return
}
Результат: каждый экземпляр Counter имеет собственный внутренний state. Это особенно удобно, если на странице несколько независимых счётчиков.
Формы: когда state кажется естественным на родителе
Частая ситуация — форма: parent хранит объект formData и передаёт data + updateData в LoginForm. Пример:
import { useState } from "react";
import { LoginForm } from "./LoginForm";
function App() {
const [formData, setFormData] = useState({
email: "",
password: "",
});
function updateFormData(newData) {
setFormData((prev) => {
return { ...prev, ...newData };
});
}
function onSubmit() {
console.log(formData);
}
return (
);
}
LoginForm получает данные и обновляет их:
function LoginForm({ onSubmit, data, updateData }) {
function handleSubmit(e) {
e.preventDefault();
onSubmit();
}
return (
);
}
Если LoginForm — единственный потребитель этих полей, имеет смысл перенести всю логику в LoginForm. Один из вариантов — использовать useRef, чтобы уменьшить количество перерисовок:
import { useRef } from "react";
function LoginForm({ onSubmit }) {
const emailRef = useRef();
const passwordRef = useRef();
function handleSubmit(e) {
e.preventDefault();
onSubmit({
email: emailRef.current.value,
password: passwordRef.current.value,
});
}
return (
);
}
В App остаётся только функция onSubmit, которую вызывает LoginForm с данными. Это упрощает код и снижает число обновлений состояния, если вам не нужно реагировать на каждое изменение ввода.
Важно: useRef хранит значения вне цикла рендера — это даёт профит по производительности, но теряется немедленная синхронизация UI с state. Используйте ref, когда вам действительно не нужна синхронная реакция на каждое изменение.
Когда локальный state не подходит
Контрпримеры:
- Несколько разных компонентов в дереве требуют одних и тех же данных (например, список задач и компонент-индексатор количества задач). В этом случае поднимите состояние в ближайший общий предок.
- Данные должны сохраняться между страницами или закладками — тогда имеет смысл использовать глобальное хранилище или sync с сервером/localStorage.
- Сложная логика, где множество действий меняют один набор данных — удобнее централизовать через Context/Reducer/Redux для прогнозируемости.
Альтернативы и уровни зрелости управления состоянием
От простого к сложному:
- Локальный useState / useRef в компоненте. Лучше всего для изолированного поведения.
- Подъём состояния до ближайшего общего предка. Подходит для нескольких дочерних компонентов.
- Context + useReducer для средних приложений, где нужно пересылать данные через несколько уровней.
- Лёгкие сторонние сторы (Zustand, Jotai) для удобства и производительности.
- Redux / MobX для больших приложений с требованием предсказуемости, инструментов и DevTools.
Выбор должен основываться на сложности требований, не на предпочтениях команды.
Практическая методология миграции к локальному состоянию
Мини-методология (шаги):
- Найдите пропсы, которые используются только в одном компоненте.
- Перенесите useState/useRef в этот компонент.
- Удалите прокидывание setX и value из родителей.
- Запустите тесты/фикс ручной проверки поведения.
- Если родитель не использует state более нигде, удалите лишние поля и функции.
Рекомендация: делайте изменения маленькими коммитами, чтобы их можно было откатить.
Чек-лист для разработчика и ревьюера
Для разработчика:
- Компонент использует state только если он его визуально/логически требует.
- Нет лишнего протаскивания set-функций через несколько уровней.
- Формы, которые не нуждаются в немедленной валидации, используют ref вместо state, если это улучшает производительность.
Для ревьюера:
- Можно ли спрятать state глубже, не ломая поведение?
- Есть ли дублирование кода при поднятом состоянии?
- Не нарушается ли принцип единственной ответственности компонента?
Критерии приёмки
- Поведение компонентов не изменилось для пользователя после рефактора.
- Нет утечки props (props drilling) более чем на 1–2 уровня туда, где можно избежать.
- Тесты покрывают критичные сценарии ввода и отправки форм.
Decision flowchart
flowchart TD
A[Нужны ли данные в нескольких компонентах?] -->|Да| B[Найти ближайшего общего предка]
B --> C{Пара компонентов или приложение-wide}
C -->|Пара| D[Поднять state в общий предок]
C -->|Application-wide| E[Рассмотреть Context/Redux/Zustand]
A -->|Нет| F[Держать state локальным в компоненте]
E --> G[Выбрать инструмент по сложности и требованиям]Тесты и критерии приёмки для примеров
Тестовые сценарии для Counter:
- Нажатие “+” увеличивает отображаемое число на 1.
- Нажатие “-“ уменьшает число на 1.
- Несколько экземпляров Counter на странице работают независимо.
Для LoginForm:
- При вводе email/password и сабмите вызывается onSubmit с корректными полями.
- При использовании useRef не происходит лишних ререндеров при каждом вводе (инструментально можно проверить через профайлер).
Меры предосторожности и подводные камни
- Не переносите логику в локальное состояние, если родитель должен валидировать/агрегировать данные.
- При использовании ref вы теряете реактивность на каждое изменение — это может повлиять на немедленную валидацию или отображение ошибок.
- Локализация state не означает отказ от хорошей архитектуры: всё ещё полезно иметь одно место для сложной бизнес-логики.
Советы по производительности
- Используйте useRef для неинтерактивных полей, которые не влияют на отображение во время ввода.
- Мемоизируйте обработчики и вычисления (useCallback, useMemo) только при реальной необходимости.
- Избегайте лишних подъемов состояния, которые приводят к массовым перерисовкам многих дочерних компонентов.
Короткий словарь
- state: локальные данные компонента, которые влияют на рендер.
- props drilling: множество уровней передачи props вниз по дереву.
- ref: ссылка на DOM-элемент или мутабельный контейнер, не вызывающий ререндер при изменении.
Короткое резюме
- Держите state как можно ближе к месту использования.
- Поднимайте состояние только тогда, когда это действительно необходимо для нескольких компонентов.
- Для форм используйте ref, если хотите уменьшить число ререндеров и не нуждаетесь в немедленной синхронизации UI.
- Рассмотрите Context/Redux/современные сторы для широкого разделения состояния.
Полезно иметь правило в проекте: сначала пробовать локальный state, и только если есть явная потребность — рефакторить в сторону поднятия состояния или глобального хранилища.
Похожие материалы
Trello для фрилансера — управление проектами и клиентами
Идеальная фотосессия беременных: 6 ключевых советов
Слои в фотографии: добавить глубину и выразительность
Как делать лучшие headshot-портреты
Как снимать отличные фото на вечеринке