Делайте состояние локальным в React — почему и как

Зачем держать состояние локальным
Если вы часто пишете на React, вероятно, сталкивались с ситуациями, когда состояние хранится в верхнем компоненте и передаётся по цепочке пропсов вниз. Это работает, но приводит к нескольким проблемам:
- лишним пропсам и «prop drilling»;
- усложнению родительского компонента ради одного дочернего;
- трудностям с повторным использованием компонента;
- потенциальным перерисовкам большого куска дерева при изменении состояния.
Коротко: храните состояние там, где его используют. Это повышает инкапсуляцию, делает логику компонент проще и снижает когнитивную нагрузку.
Простой пример — счётчик
Ниже — обычный пример, где состояние определяется в родительском компоненте и передаётся в Counter.
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}
)
}Если никакие другие компоненты не нуждаются в count, то хранение состояния в App — избыточно.
Перенос состояния в дочерний компонент
Гораздо чище, когда Counter сам управляет своим состоянием:
import {useState} from 'react'
function Counter() {
const [count, setCount] = useState(0)
return (
{count}
)
}Теперь App прост и не передаёт пропсы:
function App(){
return
}Преимущества:
- два счётчика на странице будут независимы;
- компонент сам отвечает за своё внутреннее состояние;
- проще тестировать и переиспользовать компонент.
Формы: когда локальность противоречива
Формы — типичный пример: иногда удобно держать данные формы в родительском компоненте (например, для предварительной загрузки, валидации на уровне страницы или сохранения черновика). Но часто форма — это автономная сущность. Вот исходный пример, где состояние формы лежит в App и прокидывается в 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 в этом варианте принимает data и updateData:
function LoginForm({ onSubmit, data, updateData }) {
function handleSubmit(e) {
e.preventDefault();
onSubmit();
}
return (
);
}Если форма используется только внутри LoginForm, лучше перенести логику туда. Один из простых способов — заменить управляемые контролы на refs:
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:
function App() {
function onSubmit(formData) {
console.log(formData);
}
return ;
}Плюсы использования useRef в форме:
- уменьшение количества перерисовок при каждом вводе (нет контролируемых value);
- проще код, если не нужна немедленная валидация или авто-сохранение;
- компонент остаётся автономным.
Важно: refs удобны для производительности и простых форм, но если вам нужна немедленная синхронизация поля с UI (валидация «на лету», подсказки) — контролируемые компоненты (useState) всё ещё предпочтительны.
Общие состояния, которые всё же нужно поднимать
Иногда несколько компонентов зависят от одних и тех же данных. Пример:
function TodoContainer() {
const [todos, setTodos] = useState([])
return (
<>
>
)
}В таких случаях состояние должно жить в ближайшем общем предке — здесь это TodoContainer. Перемещение состояния в App создаст излишнюю область видимости и не будет «локальным» к компонентам, которые его используют.
Для больших приложений useState/установка состояния в отдельных компонентах может стать громоздкой: тогда имеет смысл перейти к Context API или управляемому стору (Redux, Zustand, Valtio и т. п.).
Когда локальное состояние — не лучший выбор (примеры)
- Данные, которые нужны на многих страницах (аутентификация, настройки пользователя) — лучше хранить глобально.
- Очень глубокое дерево компонентов, где поднимание состояния вызывает prop drilling — используйте Context или локальный стор.
- Сложная синхронизация с сервером и кэширование — стоит рассмотреть state managers и решения типа React Query.
Альтернативные подходы
- Context API — для средних сценариев, когда нужно поделиться состоянием между разными ветками дерева.
- Redux / MobX / Zustand — для сложной логики, сложных трансформаций и предсказуемости.
- React Query / SWR — для управления асинхронными данными и кэширования.
- Комбинация: локальное состояние для UI, глобальное для бизнес-данных.
Практическая методология (мини-плейбук)
- Начните с локального состояния внутри компонента, который его использует.
- Если два или более соседних компонента нуждаются в одном и том же состоянии — поднимите его к ближайшему общему предку.
- Если вы оказываете prop drilling через несколько уровней — подумайте о Context для этой части дерева.
- Если логика состояния сложна, имеет много побочных эффектов или требует централизованного отладки — выберите стор (Redux/Zustand).
- Для форм: используйте контролируемые компоненты (useState) при необходимости валидации в реальном времени; useRef — для простых форм и оптимизации перерисовок.
Чеклист для разработчика
- Состояние используется только в одном компоненте? — держите локально.
- Нужны ли данные более чем в одном компоненте, и есть ли общий родитель? — поднимите к ближайшему общему родителю.
- Происходит ли prop drilling через >2 уровней? — подумайте о Context.
- Логика состояния сложная и критична? — рассмотрите стор.
- Нужна ли производительность при больших формах? — попробуйте useRef.
Ментальные модели и эвристики
- Правило ближайшей ответственности: состояние принадлежит компоненту, который отвечает за данные.
- Минимальная область видимости: ограждайте состояние так, чтобы по возможности меньше кода зависело от него.
- Локальность ≠ приватность: локальное состояние инкапсулирует логику, но при необходимости его можно поднять.
Советы по миграции и совместимости
- Миграция из глобального состояния в локальное: найди компоненты, которые единственные читают/пишут состояние — перенеси туда логику и адаптируй API.
- При переходе с controlled inputs на refs — проверь валидацию и UX (фокус, подсказки, подсчёт символов).
- Если используете TypeScript — явно типизируйте состояния и параметры функций onSubmit/handlers.
Факто-бокс
- useState — самый простой и распространённый хук для локального состояния.
- useRef — подходит для ссылок на DOM и хранения мутабельных значений без перерисовки.
- Context — удобен для передачи данных через дерево без пропсов.
- Редукс/сторы нужны при сложной логике, синхронизации и масштабировании.
Краткое резюме
Делайте состояние как можно локальнее. Это улучшает модульность, облегчает тестирование и уменьшает зависимость между компонентами. Поднимайте состояние только тогда, когда это действительно нужно. Для сложных сценариев применяйте Context или менеджеры состояния.
Важное: нет универсального рецепта — выбирайте подход в зависимости от размеров и требований приложения.
Похожие материалы
Бесконечная прокрутка на HTML/CSS/JS
Как измерить потребление электроэнергии ПК — методы и расчёты
React Native Elements: быстрый старт и темизация
Тёмная тема в Vue — CSS-переменные и LocalStorage
Развёртывание React на GitHub Pages