Если пропсы представляют собой неизменяемые данные, передаваемые компоненту извне, то состояние (State) — это собственная внутренняя оперативная память компонента. Состояние хранит данные, которые могут изменяться во времени в ответ на действия пользователя, сетевые события или таймеры.
В функциональных компонентах состояние объявляется с помощью базового хука useState:
const [state, setState] = useState(initialState);
Понимание работы состояния в React требует освоения трех фундаментальных архитектурных концептов:
setState в единый цикл обновления для предотвращения лишних холостых ререндеров браузера.Object.is(oldState, newState)). Прямая мутация полей объекта или элементов массива игнорируется движком и не вызывает перерисовку UI.Взглянем на рендеринг как на последовательность моментальных фотографий (снапшотов):
+-------------------------------------------------------------------------+
| МОДЕЛЬ СНАПШОТОВ (State as a Snapshot) |
+-------------------------------------------------------------------------+
| РЕНДЕР #1: count = 0 |
| |
| function Counter() { |
| const [count, setCount] = useState(0); // count зафиксирован как 0! |
| |
| function handleClick() { |
| setCount(count + 1); // Запрос: "в следующем кадре count = 1" |
| setCount(count + 1); // Запрос: "в следующем кадре count = 1" |
| console.log(count); // Напечатает: 0! (в этом снимке count = 0) |
| } |
| } |
+-------------------------------------------------------------------------+
| ОЧЕРЕДЬ ОБНОВЛЕНИЙ И ФУНКЦИОНАЛЬНЫЙ АПДЕЙТЕР: |
| setCount(prev => prev + 1); // 0 + 1 = 1 |
| setCount(prev => prev + 1); // 1 + 1 = 2 |
| (React передает актуальное промежуточное значение из очереди очереди) |
+-------------------------------------------------------------------------+Обработчик клика вызывает функцию обновления, добавляя действие в очередь обновлений очереди Fiber узла.
React группирует обновления (Batching) и планирует запуск следующего кадра с приоритетом Lanes.
Компонент вызывается заново. Хук useState возвращает новое актуальное значение состояния.
Если Object.is зафиксировал новую ссылку, React точечно мутирует DOM и обновляет экран.
Что выведет console.log в следующем обработчике при клике, если текущее значение count равно 5: `function handleClick() { setCount(count + 1); console.log(count); }`?
value={query} onChange={e => setQuery(e.target.value)}).isOpen, activeTabId).currentPage) и выбранные категории каталога.При обновлении объектов нельзя мутировать существующие свойства. Необходимо всегда создавать новый объект через спред-оператор (...):
import React, { useState } from 'react';
interface UserSettings {
theme: 'light' | 'dark';
notifications: {
email: boolean;
push: boolean;
};
}
export function SettingsForm() {
const [settings, setSettings] = useState<UserSettings>({
theme: 'dark',
notifications: { email: true, push: false },
});
const togglePushNotifications = () => {
// ПРАВИЛЬНО: поверхностное копирование внешнего объекта И вложенного объекта!
setSettings(prev => ({
...prev,
notifications: {
...prev.notifications,
push: !prev.notifications.push,
},
}));
};
// НЕПРАВИЛЬНО (Мутация ломает рендер):
// settings.notifications.push = true;
// setSettings(settings); // React видит ту же ссылку и НЕ перерисует компонент!
return (
<button onClick={togglePushNotifications} className="btn-base">
Push-уведомления: {settings.notifications.push ? 'ВКЛ' : 'ВЫКЛ'}
</button>
);
}Для массивов запрещены мутирующие методы (push, pop, splice, reverse). Используются чистые методы, возвращающие новые массивы:
interface TodoItem {
id: string;
text: string;
isDone: boolean;
}
// 1. Добавление элемента: спред-оператор [...] вместо push()
const addTodo = (newText: string) => {
const newTodo: TodoItem = { id: crypto.randomUUID(), text: newText, isDone: false };
setTodos(prev => [...prev, newTodo]);
};
// 2. Удаление элемента: метод filter() вместо splice()
const deleteTodo = (targetId: string) => {
setTodos(prev => prev.filter(item => item.id !== targetId));
};
// 3. Обновление элемента: метод map()
const toggleTodo = (targetId: string) => {
setTodos(prev =>
prev.map(item =>
item.id === targetId ? { ...item, isDone: !item.isDone } : item
)
);
};Если начальное состояние требует тяжелых вычислений (чтение localStorage, парсинг JSON), передавайте функцию-инициализатор:
// НЕПРАВИЛЬНО: парсинг будет вызываться при КАЖДОМ ререндере!
// const [data, setData] = useState(JSON.parse(localStorage.getItem('key') || '{}'));
// ПРАВИЛЬНО: функция выполнится строго ОДИН раз при монтировании!
const [data, setData] = useState(() => {
if (typeof window === 'undefined') return {};
try {
const raw = localStorage.getItem('app_cache');
return raw ? JSON.parse(raw) : {};
} catch {
return {};
}
});Код вызывает `setCount(count + 1)` три раза подряд внутри одного клика. Чему будет равен счетчик, если исходное значение было 0?
Мутация объекта по месту приводит к тому, что Object.is(oldState, newState) возвращает true, и React пропускает фазу рендера:
// ОШИБКА:
user.name = 'Иван';
setUser(user); // Компонент НЕ перерисуется!
// ПРАВИЛЬНО:
setUser({ ...user, name: 'Иван' });Не дублируйте данные в стейте, если их можно вычислить «на лету» во время рендера:
// ПЛОХО: два зависимых стейта требуют сложной синхронизации
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState(''); // Лишний стейт!
// ПРАВИЛЬНО: вычисляйте во время рендера
const fullName = `${firstName} ${lastName}`.trim();