React Hooks — це функції, які дозволяють використовувати стан, ефекти, контекст та інші можливості React в функціональних компонентах, без написання класів. Представлені в React 16.8, хуки радикально змінили підхід до розробки, замінивши складні патерни на кшталт HOC та render props. За даними React, 2024, існує 15 вбудованих хуків, що покривають усі сценарії управління компонентами.
Головне
React Hooks — це набір функцій, вбудованих в 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). Повернення функції з ефекту — це очищення, яке виконується при розмонтуванні (аналог 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)]);
};
// Очищення - закрити сокет
return () => socket.close();
}, [roomId]);
return <ul>
{messages.map((msg, i) =>
<li key={i}>{msg.text}</li>
)}
</ul>;
}
Залежності useEffect — це єдиний зв’язок між ефектом та зовнішнім світом. Якщо в ефекті використовується змінна з props або стану, вона повинна бути в масиві залежностей. Ігнорування цього правила є джерелом помилок зі застарілими замиканнями (stale closures).
useContext — хук для читання значення контексту. Він приймає об’єкт контексту, створений за допомогою React.createContext, та повертає поточне значення. Якщо контекст змінюється, компонент автоматично перерендерюється з новим значенням.
До появи useContext доступ до контексту в функціональних компонентах потребував обгортки в Context.Consumer, що створювало вкладеність. useContext повністю усуває Consumer — достатьньо викликати хук в будь-якому місці функціонального компонента.
import React, { createContext, useContext } from 'react';
const ThemeContext = createContext('світлий');
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button
className={`btn btn-${theme}`}
>
{theme === 'dark' ? 'moon' : '☀️'}
</button>;
}
useContext — це не заміна Redux або глобального стану. React рекомендує використовувати контекст для рідко змінюваних значень — теми, локалі, статус авторизації. Для часто оновлюваних даних кожен перерендер переобчислюватиме всіх споживачів контексту, що може знизити продуктивність.
useReducer — альтернатива useState для станів зі складною логікою оновлення. Він приймає функцію-редюсер та початковий стан, повертає поточний стан та функцію dispatch. Редюсер отримує поточний стан та дію, повертає новий стан — чиста функція без побічних ефектів.
useReducer особливо корисний, коли кілька дій (set, reset, increment, addItem) керують одним станом. Замість кількох useState з різними сетерами — один редюсер з чіткими типами дій. Це робить логіку предбачуваною та легко тестованою.
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);
// Автофокус при монтуванні
useEffect(() => {
inputRef.current?.focus();
}, []);
// Замемоізувати відфільтрований результат
const filtered = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase()
.includes(query.toLowerCase())
);
}, [items, query]);
// Замемоізувати колбек
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 без залежностей змінює стан | Зазначте залежності або використовуйте useReducer |
| Зміна ref в render | Побічний ефект в тілі функції | Виносьте мутації в useEffect |
Друга часта помилка — stale closure в useEffect. Якщо в ефекті використовується змінна, не зазначена в залежностях, ефект «захоплює» її значення на момент створення замикання. Рішення — завжди включайте всі використані змінні в масив залежностей. Якщо залежностей забагато, розгляньте можливість розділу ефекту на кілька.
Часто задавані питання
React покладається на порядок виклику хуків між рендерами. Якщо хук викликається всередині умови та не спрацювує на одному з рендерів, усі наступні хуки зсуваються — їхній стан перемішується. Правило ESLint hooks/exhaustive-deps попереджає про це.
useLayoutEffect синхронний і виконується до того, як браузер намалює зміни. useEffect асинхронний — після відмалювання. Використовуйте useLayoutEffect лише коли потрібно виміряти або змінити DOM до того, як користувач побачить зміни (popover позиція, відновлення прокрутки).
Використовуйте AbortController всередині useEffect. Створіть controller, передайте signal в fetch, і викличіть controller.abort() в функції очищення. Для старих браузерів використовуйте прапорець cancelled: return () => { cancelled = true; } з перевіркою перед setState.
Кастомний хук — це функція JavaScript з префіксом use, яка використовує вбудовані хуки. Створюйте його, коли одна й та сама логіка з хуками повторюється в двох або більше компонентах. Приклади: useDebounce, useLocalStorage, useMediaQuery. Кастомні хуки замінюють HOC та render props.
В Strict Mode React навмисно монтує та демонтує компонент двічі в режимі розробки, щоб знайти помилки з неочищеними ефектами. Якщо очищення написано правильно, подвійний виклик не викликає проблем. У виробничому режимі цього не відбувається.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також