Пошук уроків, статей та іншого контенту
Як виносити повторювану логіку стану в перевикористовувані функції — і правила, яких вони мають дотримуватись.
Коли та сама комбінація useState/useEffect (і логіки навколо них) повторюється в кількох компонентах — наприклад, підписка на розмір вікна браузера, дебаунсинг значення поля пошуку, збереження значення в localStorage — цю логіку можна винести в окрему функцію й перевикористовувати, замість копіювання коду між компонентами.
Технічно власний хук — просто функція, що всередині викликає інші хуки (useState, useEffect тощо) і сама повертає якесь значення чи набір значень. Назва обов'язково починається з use — це і конвенція, і технічна вимога: лінтер React (eslint-plugin-react-hooks) використовує саме цей префікс, щоб перевіряти дотримання правил хуків у функції.
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// Використання — виглядає й поводиться як звичайний useState
function Settings() {
const [theme, setTheme] = useLocalStorage("theme", "light");
return <button onClick={() => setTheme("dark")}>Тема: {theme}</button>;
}Функції debounce/throttle з основного курсу JavaScript (стаття «Debounce та throttle у JavaScript») природно виносяться в React саме у вигляді custom hook — компонент отримує «дебаунсене» значення й не думає про таймери та їх очищення самостійно:
function useDebounce(value, delay) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id); // скасовує попередній таймер при новій зміні value
}, [value, delay]);
return debounced;
}
function SearchBox() {
const [query, setQuery] = useState("");
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) fetch(`/api/search?q=${debouncedQuery}`);
}, [debouncedQuery]);
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}Хуки (вбудовані й власні) мають дотримуватись двох правил, які й дозволяють React коректно зіставляти виклики хуків між рендерами:
Викликати хуки лише на верхньому рівні функції — не всередині if, циклів чи вкладених функцій. React відстежує хуки за порядком виклику, і умовний виклик зсуває цей порядок між рендерами.
Викликати хуки лише з React-компонентів чи інших хуків — не зі звичайних функцій JavaScript.
Ці правила — не довільні обмеження, а наслідок того, як React усередині зіставляє стан кожного виклику useState/useEffect із конкретним компонентом: за порядком виклику в межах рендеру, без імен. Порушення правил ламає це зіставлення.
Виклик хука всередині if чи циклу — eslint-plugin-react-hooks одразу підсвітить це як помилку, і не випадково: порядок хуків між рендерами має лишатись однаковим.
Ім'я функції без префікса use — лінтер більше не розпізнає її як хук і не перевіряє дотримання правил хуків усередині, хоча вона технічно й далі викликає useState/useEffect.
Дублювання однієї й тієї самої логіки стану в кількох компонентах замість виносу в custom hook — не помилка як така, але втрачена можливість зробити код підтримуванішим, і саме для цього custom hooks існують.
Custom hook — звичайна функція з префіксом use, що всередині викликає інші хуки й дозволяє перевикористовувати логіку стану між компонентами (localStorage-синхронізація, debounce, підписки) без дублювання коду. Правила хуків — виклик лише на верхньому рівні й лише з React-функцій — забезпечують коректне зіставлення стану між рендерами і діють однаково і для вбудованих, і для власних хуків.