Хуки React — это функции, которые позволяют использовать состояние, эффекты, контекст и другие возможности React в функциональных компонентах, без написания классов. Представленные в React 16.8, хуки радикально изменили подход к разработке, заменив сложные паттерны вроде HOC и render props. По данным React, 2024, существует 15 встроенных хуков, покрывающих все сценарии управления компонентом.
Главное
Хуки React — набор функций, встроенных в React, которые позволяют «подключаться» к внутренним механизмам фреймворка без создания классов. До React 16.8 функциональные компоненты могли только принимать props и возвращать JSX — любая сложная логика требовала классового компонента.
Проблема классовых компонентов — разделение связанной логики. Код, относящийся к одной сущности (например, подписка на WebSocket), был вынужденно разбит между componentDidMount, componentDidUpdate и componentWillUnmount. Хуки решают это: вся логика группируется в одном useEffect.
Вторая проблема — переиспользование логики. Для повторного использования стейт-логики между классовыми компонентами приходилось применять HOC (компоненты высшего порядка) или render props — оба паттерна создавали лишнюю вложенность в дереве компонентов. Кастомные хуки решают эту проблему без вложенности.
useState — самый базовый хук. Он принимает начальное значение и возвращает кортеж: текущее значение и функцию для его обновления. В отличие от this.setState в классах, useState не сливает объекты — новое состояние полностью заменяет предыдущее для конкретного вызова useState.
import React, { useState } from 'react';
function Form() {
const [text, setText] = useState('');
const [submitting, setSubmitting] = useState(false);
async function handleSubmit() {
setSubmitting(true);
await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify({ text })
});
setSubmitting(false);
}
return (
<div>
<input
value={text}
onChange={e => setText(e.target.value)}
/>
<button onClick={handleSubmit}
disabled={submitting}>
{submitting ? 'Saving...' : 'Submit'}
</button>
</div>
);
}
Сеттер useState может принимать функцию, которая получает предыдущее значение и возвращает новое. Функциональная форма обязательна, когда новое состояние вычисляется из предыдущего и возможны батчированные вызовы setState — каждый вызов получит актуальное предыдущее значение.
useEffect — хук для выполнения побочных эффектов в функциональных компонентах. Он принимает два аргумента: функцию-эффект и массив зависимостей. React выполняет эффект после каждого рендера, но при указании зависимостей — только когда любая из них изменилась.
Пустой массив зависимостей [] означает, что эффект выполняется один раз после первого рендера (аналог componentDidMount). Возврат функции из эффекта — это cleanup, который выполняется при размонтировании (аналог componentWillUnmount) и перед повторным выполнением эффекта.
import React, { useState, useEffect } from 'react';
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const socket = new WebSocket(`wss://chat/${roomId}`);
socket.onmessage = (event) => {
setMessages(prev => [...prev, JSON.parse(event.data)]);
};
// Cleanup - close socket
return () => socket.close();
}, [roomId]);
return <ul>
{messages.map((msg, i) =>
<li key={i}>{msg.text}</li>
)}
</ul>;
}
Зависимости useEffect — это единственная связь между эффектом и внешним миром. Если в эффекте используется переменная из пропсов или состояния, она должна быть в массиве зависимостей. Игнорирование этого правила — источник багов с устаревшими данными (stale closure).
useContext — хук для чтения значения контекста. Он принимает объект контекста, созданный React.createContext, и возвращает текущее значение. Если контекст меняется, компонент автоматически ререндерится с новым значением.
До появления useContext доступ к контексту в функциональных компонентах требовал обёртки в Context.Consumer, что создавало вложенность. useContext полностью устраняет Consumer — достаточно вызвать хук в любом месте функционального компонента.
import React, { createContext, useContext } from 'react';
const ThemeContext = createContext('light');
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button
className={`btn btn-${theme}`}
>
{theme === 'dark' ? 'moon' : '☀️'}
</button>;
}
useContext — это не замена Redux или глобальному стейту. React рекомендует использовать контекст для редко изменяемых значений — тема, локаль, авторизация. Для часто обновляемых данных каждый ререндер будет заново вычислять всех потребителей контекста, что может снизить производительность.
useReducer — альтернатива useState для состояний со сложной логикой обновления. Он принимает reducer-функцию и начальное состояние, возвращает текущее состояние и dispatch-функцию. Reducer получает текущее состояние и action, возвращает новое состояние — чистая функция без побочных эффектов.
useReducer особенно полезен, когда несколько действий (set, reset, increment, addItem) управляют одним состоянием. Вместо нескольких useState с разными сеттерами — один reducer с чёткими action-типами. Это делает логику предсказуемой и легко тестируемой.
import React, { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
case 'setStep':
return { ...state, step: action.step };
case 'reset':
return { count: 0, step: 1 };
default:
throw new Error('Unknown action: ' + action.type);
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, {
count: 0,
step: 1
});
return (
<>
<p>Count: {state.count}</p>
<button
onClick={() => dispatch({ type: 'increment' })}>+</button>
<button
onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</>
);
}
useRef создаёт изменяемый объект, который сохраняется между рендерами. Изменение current не вызывает ререндер. Основное применение — доступ к DOM-элементам и хранение мутабельных значений (таймеры, предыдущие значения, экземпляры библиотек).
useMemo мемоизирует результат функции. Он пересчитывает значение только когда меняется одна из зависимостей. Используйте для дорогих вычислений — фильтрация больших массивов, сортировка, форматирование данных. useCallback — частный случай useMemo для мемоизации функций.
import React, { useRef, useMemo, useCallback } from 'react';
function SearchPage({ items, query, onItemSelect }) {
const inputRef = useRef(null);
// Autofocus on mount
useEffect(() => {
inputRef.current?.focus();
}, []);
// Memoize filtered result
const filtered = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase()
.includes(query.toLowerCase())
);
}, [items, query]);
// Memoize callback
const handleSelect = useCallback((id) => {
onItemSelect(id);
}, [onItemSelect]);
return (
<div>
<input ref={inputRef}
value={query}
onChange={e => setQuery(e.target.value)}
/>
<ul>
{filtered.map(item =>
<li key={item.id}>
<button
onClick={() => handleSelect(item.id)}>
{item.name}
</button>
</li>
)}
</ul>
</div>
);
}
Важное правило: useRef, useMemo и useCallback — инструменты оптимизации, а не обязательные конструкции. Не оборачивайте всё в useMemo — измерьте производительность сначала. Преждевременная оптимизация усложняет код. React рендерит компоненты быстро; useMemo нужен только когда вычисления действительно дорогие (O(n²) и выше).
React ввёл два жёстких правила для хуков. Первое: вызывайте хуки только на верхнем уровне — не внутри условий, циклов или вложенных функций. Второе: вызывайте хуки только из функциональных компонентов React или кастомных хуков. ESLint-плагин eslint-plugin-react-hooks автоматически проверяет оба правила.
Самая частая ошибка — нарушение порядка вызова хуков. React полагается на порядок вызова хуков между рендерами. Если хук находится внутри условия и не вызвался в одном из рендеров, все последующие хуки сдвигаются, вызывая баги.
| Ошибка | Что происходит | Решение |
|---|---|---|
| Хук в условии | Порядок хуков ломается | Перенесите логику условия внутрь хука |
| Stale closure | Эффект использует старое значение | Добавьте все зависимости в массив |
| Бесконечный цикл | useEffect без зависимостей меняет state | Укажите зависимости или используйте useReducer |
| Изменение ref в render | Побочный эффект в теле функции | Выносите мутации в useEffect |
Вторая частая ошибка — stale closure в useEffect. Если в эффекте используется переменная, не указанная в зависимостях, эффект «захватывает» её значение на момент создания замыкания. Решение — всегда включать все используемые переменные в массив зависимостей. Если зависимостей слишком много — возможно, стоит разделить эффект на несколько.
Часто задаваемые вопросы
React полагается на порядок вызова хуков между рендерами. Если хук вызван в условии и не сработал на одном из рендеров, все последующие хуки «съезжают» — их состояние перемешивается. ESLint-правило hooks/exhaustive-deps предупреждает об этом.
useLayoutEffect синхронный и выполняется до того, как браузер отрисует изменения. useEffect асинхронный — после отрисовки. Используйте useLayoutEffect только когда нужно измерить или изменить DOM до того, как пользователь увидит изменения (popover position, scroll restoration).
Используйте AbortController внутри useEffect. Создайте controller, передайте signal в fetch, и в cleanup-функции вызовите controller.abort(). Для старых браузеров подойдёт флаг cancelled: return () => { cancelled = true; } с проверкой перед setState.
Кастомный хук — JavaScript-функция с префиксом use, использующая встроенные хуки. Создавайте его, когда одна и та же логика с хуками повторяется в двух и более компонентах. Пример: useDebounce, useLocalStorage, useMediaQuery. Кастомные хуки заменяют HOC и render props.
В Strict Mode React намеренно монтирует и размонтирует компонент дважды в development, чтобы найти ошибки с неочищенными эффектами. Если cleanup написан корректно, двойной вызов не вызывает проблем. В production режиме это не происходит.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также