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

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 — в банке вопросов.


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