React — хуки, состояние и формы
Маршрут React
| Шаг | Статья |
|---|---|
| 1 | Первая программа |
| 2 | Обзор |
| 3 | Компоненты и JSX |
| 4 | Вы здесь |
| 5 | Router и данные |
Перед хуками полезно понять однонаправленный поток данных и разницу между props (вход от родителя) и state (внутренние данные компонента).
Что такое хуки
Хук — функция из React, имя которой начинается с use. Хуки подключают к функциональному компоненту то, что раньше давали только классы:
- локальное состояние (state);
- побочные эффекты (запросы к API, таймеры, подписки);
- доступ к контексту и DOM без лишних обёрток.
Хуки вызывают только на верхнем уровне компонента — подробнее в разделе про правила.
Карта встроенных хуков
| Задача | Хуки |
|---|---|
| Состояние | useState, useReducer |
| Побочные эффекты | useEffect, useLayoutEffect |
| Общие данные в дереве | useContext |
| Кэш вычислений и функций | useMemo, useCallback |
| Ссылки на DOM и mutable-значения | useRef, useImperativeHandle |
| Переиспользование логики | пользовательские хуки (use…) |
Сигнатуры и краткие определения — в Справочнике по React. Живые примеры счётчика, todo, формы и Context — в галерее Lab.
useState
State — данные компонента, которые меняются со временем. При обновлении state React заново рендерит компонент (строит JSX и обновляет экран).
const [count, setCount] = useState(0);
<button onClick={() => setCount((c) => c + 1)}>{count}</button>
useState(0)возвращает пару — текущее значение и функцию-сеттер для записи нового.setCountпланирует обновление на следующий цикл рендера. После него React сравнивает новое дерево с предыдущим и правит DOM — см. Virtual DOM.- Форма
setCount((c) => c + 1)читает актуальное значение на момент обновления. Её используют, когда за один клик срабатывает несколько вызовов сеттера подряд.
Иммутабельные обновления
React сравнивает state по ссылке. Если мутировать массив или объект на месте (arr.push(x)), ссылка не меняется и экран может не обновиться.
- создавайте новый массив —
[...arr, x]; - создавайте новый объект —
{ ...obj, field: value }.
Таблица мутирующих и иммутабельных методов — в типах данных JavaScript. Типичные ошибки с state — в компонентах и JSX.
Запись state в классовых компонентах
В старых классах state лежал в this.state, обновление шло через this.setState. В функциональном компоненте ту же роль играет useState:
// класс (устаревший стиль)
this.setState(({ count }) => ({ count: count + 1 }));
// функциональный компонент
setCount((c) => c + 1);
Современные проекты пишут на функциях и хуках. Классы встречаются в legacy-коде.
useReducer
useReducer управляет state через функцию-редьюсер. Редьюсер получает текущий state и объект-действие (action), возвращает новый state. Компонент вызывает dispatch, чтобы отправить действие — сам state напрямую не трогает.
Подходит, когда:
- в state несколько связанных полей;
- переходы описываются правилами ("если нажали +, увеличить счётчик");
- одна и та же логика понадобится в разных местах (похоже на Redux, но локально в компоненте).
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error('Неизвестное действие');
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
<p>Счёт: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>−</button>
</>
);
}
Для простого счётчика достаточно useState. useReducer удобен, когда правил обновления становится много.
useEffect
Побочный эффект — любое действие за пределами чистого рендера:
- сетевой запрос;
- подписка на событие или WebSocket;
- таймер;
- запись в
document.titleилиlocalStorage.
useEffect запускает такой код после того, как React отрисовал компонент на экране.
useEffect(() => {
document.title = `Счёт: ${count}`;
}, [count]);
Второй аргумент — массив зависимостей (deps). React сравнивает значения из массива с прошлым рендером и решает, запускать эффект снова или пропустить.
| Массив зависимостей | Когда срабатывает эффект |
|---|---|
[count] | при изменении count |
[] | один раз после первого появления компонента (монтирование) |
| массив не передан | после каждого рендера |
Пустой массив [] часто используют для загрузки данных при открытии страницы — пример со списком и fetch в галерее Lab и в первой программе.
Очистка эффекта
Эффект может вернуть функцию очистки (cleanup). React вызывает её:
- перед следующим запуском того же эффекта;
- когда компонент исчезает с экрана (размонтирование).
useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);
Без clearInterval таймер продолжит работать после ухода со страницы — типичная утечка. Тот же принцип для подписок и addEventListener.
Частая ошибка — бесконечный цикл
Если внутри useEffect вызывать setState, а массив зависимостей пустой или отсутствует, цепочка "рендер → эффект → setState → рендер" может не остановиться. Указывайте в deps только те значения, от которых реально зависит код эффекта.
Пример подписки
Компонент показывает, онлайн ли друг. Нужно подписаться на API при появлении на экране и отписаться при смене друга или уходе:
function FriendStatus({ friend }) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friend.id, handleStatusChange);
};
}, [friend.id]);
if (isOnline === null) return 'Загрузка…';
return isOnline ? 'В сети' : 'Не в сети';
}
friendприходит сверху через props — см. поток данных.[friend.id]в deps перезапускает эффект при смене друга: сначала cleanup (отписка), затем новая подписка.- В классовых компонентах ту же схему разносили по
componentDidMount,componentDidUpdateиcomponentWillUnmount. В хуках всё собрано в одномuseEffect.
Загрузка с сервера и состояния loading / error — в Router, данные и оптимизация.
useLayoutEffect
Синтаксис как у useEffect, но колбэк выполняется сразу после обновления DOM и до того, как браузер покажет кадр пользователю.
Применяют, когда нужно:
- измерить размеры элемента (
getBoundingClientRect); - синхронно поправить стили или позицию до отрисовки;
- убрать визуальный "миг" при быстрой смене данных.
function ColoredBlock({ color }) {
const ref = useRef(null);
useLayoutEffect(() => {
ref.current.style.borderColor = color;
}, [color]);
return <div ref={ref} style={{ color }}>Текст</div>;
}
useEffect с тем же кодом сработал бы уже после paint — пользователь мог бы на долю секунды увидеть старый border. В большинстве задач достаточно useEffect. useLayoutEffect подключайте, когда заметили мерцание или нужны точные замеры layout.
useContext
Контекст передаёт значение вглубь дерева компонентов без цепочки props через каждый уровень. Такую цепочку называют props drilling — когда промежуточные компоненты получают данные только ради передачи ниже.
Типичные данные для контекста:
- тема оформления (светлая / тёмная);
- язык интерфейса;
- данные авторизованного пользователя.
Схема работы:
createContext(defaultValue)— создать объект контекста.<ThemeContext.Provider value={theme}>— обернуть поддерево и передать значение.useContext(ThemeContext)— прочитать значение в любом потомке внутри Provider.
const ThemeContext = createContext('light');
function Header() {
const theme = useContext(ThemeContext);
return <header className={theme}>…</header>;
}
В аргумент useContext передают объект из createContext:
const theme = useContext(ThemeContext); // верно
const theme = useContext(ThemeContext.Consumer); // неверно — Consumer это компонент-обёртка
В классах контекст читали через вложенные <Context.Consumer>. С хуком код плоский:
const { isLoggedIn } = useContext(AuthContext);
const { showModal } = useContext(ModalContext);
Готовый пример тёмной темы через Context — в галерее Lab. Provider, Consumer и подробности API — в справочнике.
Для часто меняющегося глобального state (корзина, сложные формы на весь экран) чаще берут Zustand или Redux Toolkit — см. интеграцию со store. Context хорошо подходит для редко меняющихся настроек.
useMemo и useCallback
При каждом рендере функции и объекты в JavaScript создаются заново — новая ссылка в памяти, даже если содержимое то же. Дочерний компонент, обёрнутый в React.memo, сравнивает props по ссылке и может лишний раз перерисоваться.
Мемоизация сохраняет результат или функцию между рендерами, пока не изменились значения из массива зависимостей.
| Хук | Что сохраняется | Пример |
|---|---|---|
useMemo | результат вычисления | отфильтрованный список из тысячи строк |
useCallback | ссылка на функцию | обработчик, передаваемый в memo-компонент |
function RandomColoredLetter() {
const [color, setColor] = useState('#fff');
const [letter, setLetter] = useState('a');
const handleColorChange = useCallback(() => setColor(randomColor()), []);
const handleLetterChange = useCallback(() => setLetter(randomLetter()), []);
return (
<>
<ColorPicker onChange={handleColorChange} color={color} />
<LetterPicker onChange={handleLetterChange} letter={letter} />
<h1 style={{ color }}>{letter}</h1>
</>
);
}
useCallback(fn, deps) технически эквивалентен useMemo(() => fn, deps).
Подключайте мемоизацию после замера в React DevTools Profiler — см. оптимизацию и React.memo в справочнике. Преждевременная мемоизация усложняет код без выигрыша.
useRef
useRef создаёт объект { current: … }, который живёт между рендерами. Изменение ref.current не запускает новый рендер.
Две основные задачи:
- ссылка на DOM-узел — фокус поля ввода, прокрутка, интеграция с chart-библиотекой;
- хранение mutable-значения — id таймера, предыдущее значение prop, флаг "уже отправили запрос".
function TextInput() {
const inputRef = useRef(null);
const onFocus = () => inputRef.current?.focus();
return (
<>
<input ref={inputRef} />
<button type="button" onClick={onFocus}>Фокус</button>
</>
);
}
Атрибут ref на JSX-элементе связывает узел DOM с inputRef.current. Оператор ?. безопасно вызывает метод, если ref ещё не привязан.
Неконтролируемые поля, где значение читают из DOM через ref, — в справочнике по формам.
useImperativeHandle
По умолчанию ref на дочернем компоненте даёт доступ ко всему его DOM. useImperativeHandle ограничивает публичный API — родитель видит только те методы, которые вы явно отдали.
Работает вместе с forwardRef, который пробрасывает ref в функциональный компонент:
const TextInput = forwardRef(function TextInput(props, ref) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
}));
return <input ref={inputRef} {...props} />;
});
function Parent() {
const ref = useRef(null);
useEffect(() => {
ref.current?.focus();
}, []);
return <TextInput ref={ref} />;
}
Родитель вызывает ref.current.focus(), не зная внутренней разметки TextInput. Паттерн встречается в полях ввода, модалках и обёртках над сторонними виджетами.
Пользовательские хуки
Пользовательский хук — обычная функция с именем use…, внутри которой вызываются другие хуки. Так выносят повторяющуюся логику из компонентов.
Пример useHover — отслеживает наведение мыши на элемент:
function useHover() {
const [hovered, setHovered] = useState(false);
const ref = useRef(null);
const onMouseOver = useCallback(() => setHovered(true), []);
const onMouseOut = useCallback(() => setHovered(false), []);
useEffect(() => {
const node = ref.current;
if (!node) return;
node.addEventListener('mouseover', onMouseOver);
node.addEventListener('mouseout', onMouseOut);
return () => {
node.removeEventListener('mouseover', onMouseOver);
node.removeEventListener('mouseout', onMouseOut);
};
}, [onMouseOver, onMouseOut]);
return [ref, hovered];
}
function HoverableLabel() {
const [ref, isHovered] = useHover();
return (
<span ref={ref} style={{ color: isHovered ? 'blue' : 'inherit' }}>
Наведи курсор
</span>
);
}
Хук возвращает ref для навешивания на JSX и флаг hovered. Компонент остаётся коротким, детали слушателей спрятаны в useHover.
Другие готовые шаблоны — в разделе пользовательских хуков:
useLocalStorage— синхронизация state с браузерным хранилищем;useDebounce— задержка перед реакцией на ввод;useOnClickOutside— клик вне элемента.
Пример useLocalStorage в действии — в галерее Lab. Вопросы для самопроверки по хукам — в банке вопросов.
События
Обработчики в JSX пишут в camelCase и получают ссылку на функцию:
<button onClick={() => setCount((c) => c + 1)}>+</button>
В HTML атрибут onclick иногда содержал строку кода. В React в onClick передают функцию из JavaScript.
React оборачивает нативные события в SyntheticEvent — единый объект с одинаковым API в разных браузерах. Обработчики на многих элементах делегируются на корень приложения, что снижает нагрузку на память.
В формах e.preventDefault() отменяет стандартное поведение браузера (например, перезагрузку страницы при submit). Полный список событий — в справочнике.
Контролируемые формы
Контролируемый компонент — поле ввода, значение которого хранится в React state. Экран и state всегда совпадают — это единый источник правды.
const [text, setText] = useState('');
<input
value={text}
onChange={(e) => setText(e.target.value)}
/>
value={text}— React рисует то, что в state.onChange— при вводе записывает новое значение через сеттер.
Разбор controlled input на примере имени — в галерее Lab. Концепция контролируемого компонента из компонентов и JSX.
Несколько полей
Один объект state и общий обработчик:
const [form, setForm] = useState({ name: '', email: '' });
const handleChange = (e) => {
const { name, value } = e.target;
setForm((prev) => ({ ...prev, [name]: value }));
};
У каждого <input> атрибут name совпадает с ключом в объекте (name, email).
Для форм с валидацией, масками и множеством полей удобны библиотеки React Hook Form, Formik с Zod — см. управление формами в справочнике.
Правила хуков
- Вызывайте хуки только на верхнем уровне тела компонента — вне
if, циклов и вложенных функций. - Вызывайте хуки только из React-компонентов или других пользовательских хуков.
React запоминает хуки по порядку вызова между рендерами. Условный вызов нарушает этот порядок и ломает state.
Помощник в редакторе — плагин eslint-plugin-react-hooks (правила rules-of-hooks и exhaustive-deps). Подробный Q&A — в банке вопросов.
Связанные материалы
- React — Router, данные с API и оптимизация — маршруты,
fetch, lazy, Profiler - Первая программа на React — проект "Заметки" на Vite
- React — компоненты, JSX и поток данных — props, state, Virtual DOM
- Справочник по React — полные таблицы API
- React — компоненты-рецепты — счётчик, todo, форма, Context, Router
- Кнопка с загрузкой и
isLoading— async и состояние загрузки