Пошук уроків, статей та іншого контенту
Навчитеся обирати між Hooks для стану, ефектів, посилань, контексту й оптимізації відповідно до завдання компонента.
React Hook варто обирати не за принципом «який Hook я вже знаю», а за типом задачі:
потрібно зберігати дані, які впливають на інтерфейс — useState або useReducer;
потрібно синхронізувати компонент із зовнішньою системою — useEffect;
потрібно зберегти значення між рендерами, але не запускати повторний рендер — useRef;
потрібно отримати спільне значення з контексту — useContext;
потрібно оптимізувати обчислення або функцію — useMemo чи useCallback.
Hook не є універсальним сховищем даних. Кожен із них має власну відповідальність.
Поставте собі такі запитання:
Це значення має відображатися в JSX?
Так — використовуйте useState або useReducer.
Ні — розгляньте useRef або звичайну локальну змінну.
Це дія синхронізує React із чимось зовнішнім?
Наприклад, із таймером, підпискою, DOM API чи мережевим з'єднанням.
Так — використовуйте useEffect.
Потрібно звернутися до DOM-елемента або зберегти змінне значення без ререндеру?
Використовуйте useRef.
Значення має бути доступним для багатьох компонентів без передавання через кожен рівень props?
Використовуйте useContext.
Обчислення або створення функції справді є дорогим?
Розгляньте useMemo або useCallback.
Не використовуйте їх автоматично для будь-якого значення.
useState: стан, який впливає на інтерфейсuseState підходить для локального стану компонента:
значення поля форми;
відкриття або закриття модального вікна;
активна вкладка;
лічильник;
стан завантаження.
Оновлення стану спричиняє повторний рендер компонента.
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
function increase() {
setCount((currentCount) => currentCount + 1);
}
return (
<section>
<p>Натискань: {count}</p>
<button onClick={increase}>
Збільшити
</button>
</section>
);
}useStateОбирайте useState, якщо:
стан належить одному компоненту;
зміна стану має оновити JSX;
логіка оновлення проста;
стан можна описати кількома незалежними значеннями.
Якщо нове значення залежить від попереднього, використовуйте функціональний запис:
setCount((previousCount) => previousCount + 1);Так React завжди отримає актуальне попереднє значення.
Кілька незалежних значень часто зрозуміліше зберігати в окремих станах:
const [isOpen, setIsOpen] = useState(false);
const [query, setQuery] = useState('');Не обов'язково об'єднувати їх в один об'єкт лише тому, що вони належать одному компоненту.
useReducer: складні переходи стануuseReducer корисний, коли:
стан має кілька пов'язаних полів;
існує багато типів змін;
логіку оновлення потрібно зібрати в одному місці;
наступний стан залежить від дії та поточного стану.
Редюсер — це функція, яка отримує поточний стан і дію, а повертає новий стан.
import { useReducer } from 'react';
const initialState = {
items: [],
isSubmitting: false,
error: null,
};
function formReducer(state, action) {
switch (action.type) {
case 'submit_started':
return {
...state,
isSubmitting: true,
error: null,
};
case 'submit_succeeded':
return {
...state,
isSubmitting: false,
items: [...state.items, action.item],
};
case 'submit_failed':
return {
...state,
isSubmitting: false,
error: action.error,
};
default:
throw new Error(`Невідома дія: ${action.type}`);
}
}
export default function ItemForm() {
const [state, dispatch] = useReducer(formReducer, initialState);
function addItem() {
dispatch({ type: 'submit_started' });
// Імітуємо успішне завершення операції
window.setTimeout(() => {
dispatch({
type: 'submit_succeeded',
item: `Елемент ${state.items.length + 1}`,
});
}, 500);
}
return (
<section>
<button onClick={addItem} disabled={state.isSubmitting}>
{state.isSubmitting ? 'Додавання...' : 'Додати елемент'}
</button>
{state.error && <p role="alert">{state.error}</p>}
<ul>
{state.items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</section>
);
}useReducer не є автоматично кращим за useState. Для простого прапорця isOpen редюсер буде зайвим. Його варто застосовувати тоді, коли він робить переходи стану зрозумілішими.
useEffect: синхронізація із зовнішніми системамиuseEffect призначений для коду, який синхронізує компонент із системою поза React:
підписка на події;
таймер;
робота з браузерним API;
підключення до зовнішнього сервісу;
синхронізація з DOM API;
запуск запиту, коли він справді залежить від стану компонента.
import { useEffect, useState } from 'react';
export default function WindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return <p>Ширина вікна: {width}px</p>;
}У цьому прикладі:
підписка створюється після монтування компонента;
обробник оновлює стан;
функція очищення видаляє обробник;
порожній масив залежностей означає, що ефект не залежить від пропсів або стану компонента.
useEffect не потрібенНе використовуйте ефект для обчислення значення з уже наявних props або state.
Погано:
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);fullName є похідним значенням. Його можна обчислити під час рендеру:
const fullName = `${firstName} ${lastName}`;Так код простіший, а компонент не має зайвого стану та додаткового ререндеру.
Також не варто використовувати useEffect для реакції на подію користувача, якщо дію можна виконати безпосередньо в обробнику:
function handleSubmit(event) {
event.preventDefault();
saveForm();
}Ефект потрібен для синхронізації, а не як заміна всім обробникам подій.
У масиві залежностей мають бути всі значення з компонента, які використовуються ефектом і можуть змінюватися:
useEffect(() => {
document.title = `Профіль: ${userName}`;
}, [userName]);Якщо ефект використовує userName, але залежності порожні, він може працювати зі застарілим значенням.
useRef: значення без ререндеру та посилання на DOMuseRef повертає об'єкт із властивістю .current.
Значення в .current:
зберігається між рендерами;
може змінюватися;
не спричиняє ререндер після зміни.
import { useRef } from 'react';
export default function SearchForm() {
const inputRef = useRef(null);
function focusInput() {
inputRef.current?.focus();
}
return (
<form>
<input ref={inputRef} type="search" />
<button type="button" onClick={focusInput}>
Фокус на поле
</button>
</form>
);
}useRef також може зберігати технічне значення, яке потрібне між рендерами, але не показується в інтерфейсі:
import { useEffect, useRef } from 'react';
export default function Price({ value }) {
const previousValue = useRef(value);
useEffect(() => {
previousValue.current = value;
}, [value]);
return (
<p>
Поточна ціна: {value}. Попередня: {previousValue.current}
</p>
);
}useRefНе використовуйте useRef для даних, які мають бути відображені в JSX.
Погано:
const clicks = useRef(0);
function handleClick() {
clicks.current += 1;
}Компонент не перерендериться після зміни clicks.current, тому інтерфейс не покаже нове значення. Для такого випадку потрібен useState.
useContext: спільне значення для піддереваuseContext дає компоненту доступ до значення, яке передав найближчий відповідний Provider.
Це зручно для даних, які потрібні багатьом компонентам на різних рівнях:
поточна тема;
локаль або мова інтерфейсу;
дані автентифікованого користувача;
налаштування застосунку.
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext(null);
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
function toggleTheme() {
setTheme((currentTheme) =>
currentTheme === 'light' ? 'dark' : 'light'
);
}
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
function ThemeButton() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('ThemeButton має бути всередині ThemeProvider');
}
return (
<button onClick={context.toggleTheme}>
Поточна тема: {context.theme}
</button>
);
}
export default function App() {
return (
<ThemeProvider>
<ThemeButton />
</ThemeProvider>
);
}Контекст варто обрати, якщо значення:
потрібне великій частині дерева компонентів;
логічно є спільним для певного піддерева;
незручно передавати через багато рівнів props.
Якщо дані потрібні лише дочірньому компоненту, звичайне передавання через props зазвичай зрозуміліше.
useMemo: мемоізація обчисленого значенняuseMemo кешує результат обчислення між рендерами, доки не зміняться залежності.
import { useMemo, useState } from 'react';
export default function ProductList({ products }) {
const [query, setQuery] = useState('');
const filteredProducts = useMemo(() => {
const normalizedQuery = query.trim().toLowerCase();
return products.filter((product) =>
product.name.toLowerCase().includes(normalizedQuery)
);
}, [products, query]);
return (
<section>
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Пошук товарів"
/>
<ul>
{filteredProducts.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</section>
);
}useMemo виправданийВикористовуйте useMemo, коли:
обчислення справді дороге;
результат передається компоненту, який оптимізує ререндер;
обчислення потрібно виконувати лише після зміни конкретних залежностей.
Для простого виразу мемоізація часто лише ускладнює код:
const fullName = `${firstName} ${lastName}`;Не потрібно замінювати це на useMemo.
useMemo — оптимізаційний інструмент, а не спосіб зберігати стан. Код має залишатися коректним навіть без мемоізації.
useCallback: стабільне посилання на функціюuseCallback повертає мемоізовану функцію. Її посилання залишається тим самим, доки не зміняться залежності.
Це може бути корисно, коли функція:
передається оптимізованому дочірньому компоненту;
використовується як залежність іншого Hook;
створюється з дорогими залежностями.
import { memo, useCallback, useState } from 'react';
const SaveButton = memo(function SaveButton({ onSave }) {
console.log('Рендер SaveButton');
return <button onClick={onSave}>Зберегти</button>;
});
export default function Editor() {
const [text, setText] = useState('');
const handleSave = useCallback(() => {
console.log('Збережено:', text);
}, [text]);
return (
<section>
<textarea
value={text}
onChange={(event) => setText(event.target.value)}
/>
<SaveButton onSave={handleSave} />
</section>
);
}Якщо handleSave не був би мемоізований, під час кожного рендера Editor створювалося б нове посилання на функцію. Для memo це означало б зміну props і повторний рендер SaveButton.
Водночас useCallback не потрібен для кожного обробника. Спочатку потрібно побачити реальну проблему з продуктивністю або стабільністю залежностей.
У реальному компоненті може знадобитися кілька Hooks. Важливо, щоб кожен відповідав за свою задачу.
Наприклад, компонент пошуку може:
зберігати текст у useState;
запускати запит у useEffect;
зберігати ідентифікатор таймера в useRef.
import { useEffect, useRef, useState } from 'react';
export default function Search({ searchProducts }) {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const timeoutRef = useRef(null);
useEffect(() => {
window.clearTimeout(timeoutRef.current);
if (!query.trim()) {
setResults([]);
return undefined;
}
timeoutRef.current = window.setTimeout(async () => {
const nextResults = await searchProducts(query);
setResults(nextResults);
}, 300);
return () => {
window.clearTimeout(timeoutRef.current);
};
}, [query, searchProducts]);
return (
<section>
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Знайти товар"
/>
<ul>
{results.map((result) => (
<li key={result.id}>{result.name}</li>
))}
</ul>
</section>
);
}Тут:
query потрібен у JSX, тому використовується useState;
пошук є синхронізацією зі зовнішньою функцією searchProducts, тому використовується useEffect;
ідентифікатор таймера не потрібно показувати в інтерфейсі, тому він зберігається в useRef.
Hooks потрібно викликати:
лише на верхньому рівні компонента або custom Hook;
не всередині if;
не всередині циклів;
не всередині вкладених функцій;
не після дострокового return.
Правильно:
function Profile({ user }) {
const [isEditing, setIsEditing] = useState(false);
if (!user) {
return <p>Користувача не знайдено</p>;
}
return (
<button onClick={() => setIsEditing(true)}>
{isEditing ? 'Редагування' : 'Редагувати'}
</button>
);
}Неправильно:
function Profile({ user }) {
if (user) {
const [isEditing, setIsEditing] = useState(false);
}
return null;
}React покладається на незмінний порядок викликів Hooks між рендерами. Тому умовний виклик може призвести до того, що React пов'яже стан не з тим Hook.
useEffect для похідного стануЯкщо значення можна безпосередньо обчислити з props або state, не створюйте для нього окремий стан та ефект.
const total = price * quantity;а не:
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(price * quantity);
}, [price, quantity]);useRef замість useStateЯкщо зміна значення має бути видимою в інтерфейсі, використовуйте useState.
Контекст не є заміною всім props. Локальні дані краще залишати локальними, щоб залежності компонентів були очевидними.
useMemo і useCallback додають залежності та ускладнюють код. Спочатку напишіть просту реалізацію, а потім оптимізуйте конкретне вузьке місце.
useEffectЯкщо ефект використовує змінне значення, воно зазвичай має бути в масиві залежностей. Ігнорування цього правила може призвести до роботи із застарілими даними.
Hooks викликаються під час рендеру компонента, а не під час натискання кнопки:
function Component() {
const [value, setValue] = useState('');
function handleClick() {
setValue('нове значення');
}
return <button onClick={handleClick}>Змінити</button>;
}| Завдання | Hook | |---|---| | Значення показується в JSX і змінюється | useState | | Складна логіка переходів стану | useReducer | | Синхронізація з таймером, підпискою або API | useEffect | | Доступ до DOM | useRef | | Значення між рендерами без ререндеру | useRef | | Спільне значення для піддерева | useContext | | Дороге обчислення | useMemo | | Стабільне посилання на функцію | useCallback |
Один компонент може використовувати кілька Hooks, але кожен Hook має вирішувати окрему проблему.
useState зберігає локальні дані, які впливають на інтерфейс.
useReducer підходить для складних пов'язаних переходів стану.
useEffect синхронізує компонент із зовнішніми системами.
useRef зберігає значення без ререндеру та дає доступ до DOM.
useContext передає спільні дані компонентам у піддереві.
useMemo кешує результат дорогого обчислення.
useCallback зберігає стабільне посилання на функцію.
Не використовуйте useEffect для простих похідних значень.
Не використовуйте useRef для даних, які мають оновлювати JSX.
Оптимізаційні Hooks потрібні лише тоді, коли вони розв'язують конкретну проблему.