Перейти к основному содержимому

React — хуки, состояние и формы


Маршрут React

ШагСтатья
1Первая программа
2Обзор
3Компоненты и JSX
4Вы здесь
5Router и данные

Перед хуками полезно понять однонаправленный поток данных и разницу между 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 — когда промежуточные компоненты получают данные только ради передачи ниже.

Типичные данные для контекста:

  • тема оформления (светлая / тёмная);
  • язык интерфейса;
  • данные авторизованного пользователя.

Схема работы:

  1. createContext(defaultValue) — создать объект контекста.
  2. <ThemeContext.Provider value={theme}> — обернуть поддерево и передать значение.
  3. 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 — см. управление формами в справочнике.


Правила хуков

  1. Вызывайте хуки только на верхнем уровне тела компонента — вне if, циклов и вложенных функций.
  2. Вызывайте хуки только из React-компонентов или других пользовательских хуков.

React запоминает хуки по порядку вызова между рендерами. Условный вызов нарушает этот порядок и ломает state.

Помощник в редакторе — плагин eslint-plugin-react-hooks (правила rules-of-hooks и exhaustive-deps). Подробный Q&A — в банке вопросов.


Связанные материалы