Пошук уроків, статей та іншого контенту
Познайомитеся з Custom Hooks, їхньою структурою, правилами і способом повторного використання логіки між компонентами.
Custom Hook — це JavaScript-функція, яка дає змогу винести логіку, що використовує React Hooks, і повторно застосувати її в різних компонентах.
Назва Custom Hook обов’язково починається з use. Наприклад:
useLocalStorage;
useWindowSize;
useFetch;
useForm.
Custom Hook:
може викликати вбудовані Hooks, такі як useState і useEffect;
може викликати інші Custom Hooks;
не створює розмітку;
повертає дані або функції, необхідні компоненту.
Наприклад, логіку роботи з localStorage можна винести в useLocalStorage, щоб не дублювати її в кожному компоненті.
Без Custom Hook логіку потрібно дублювати в компонентах:
function Counter() {
const [value, setValue] = useState(() => {
const storedValue = localStorage.getItem("counter");
return storedValue === null ? 0 : Number(storedValue);
});
useEffect(() => {
localStorage.setItem("counter", String(value));
}, [value]);
// ...
}Якщо іншому компоненту теж потрібно зберігати значення в localStorage, доведеться повторити цей код.
Custom Hook дає змогу залишити в компоненті тільки використання логіки:
const [value, setValue] = useLocalStorage("counter", 0);Компонент працює з готовим API, а деталі реалізації зберігаються в одному місці.
Custom Hook — це звичайна функція, яка:
починається з префікса use;
викликає один або кілька Hooks;
повертає результат, потрібний компоненту.
Приклад найпростішого Custom Hook:
import { useState } from "react";
function useCounter(initialValue = 0) {
const [value, setValue] = useState(initialValue);
function increment() {
setValue((currentValue) => currentValue + 1);
}
function decrement() {
setValue((currentValue) => currentValue - 1);
}
return {
value,
increment,
decrement,
};
}Тепер цей Hook можна використовувати в компоненті:
function Counter() {
const { value, increment, decrement } = useCounter(10);
return (
<div>
<p>Значення: {value}</p>
<button onClick={decrement}>−</button>
<button onClick={increment}>+</button>
</div>
);
}Один Custom Hook можна викликати в кількох компонентах:
function FirstCounter() {
const counter = useCounter(0);
return (
<button onClick={counter.increment}>
Перше значення: {counter.value}
</button>
);
}
function SecondCounter() {
const counter = useCounter(100);
return (
<button onClick={counter.increment}>
Друге значення: {counter.value}
</button>
);
}Важливо: кожен виклик useCounter створює власний незалежний стан.
У цьому прикладі FirstCounter і SecondCounter не використовують одне й те саме значення. Вони використовують одну логіку, але мають різні екземпляри стану.
Custom Hooks повторно використовують логіку, але не об'єднують стан між компонентами.
useLocalStorageСтворимо Hook, який синхронізує стан компонента з localStorage.
import { useEffect, useState } from "react";
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const storedValue = window.localStorage.getItem(key);
if (storedValue === null) {
return initialValue;
}
try {
return JSON.parse(storedValue);
} catch {
// Якщо значення пошкоджене, використовуємо початкове
return initialValue;
}
});
useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
function Counter() {
const [count, setCount] = useLocalStorage("counter-value", 0);
return (
<section>
<h2>Лічильник</h2>
<p>Значення: {count}</p>
<button onClick={() => setCount((currentCount) => currentCount - 1)}>
−
</button>
<button onClick={() => setCount((currentCount) => currentCount + 1)}>
+
</button>
<button onClick={() => setCount(0)}>Скинути</button>
</section>
);
}
function ThemeSelector() {
const [theme, setTheme] = useLocalStorage("selected-theme", "light");
return (
<section>
<h2>Тема</h2>
<label>
Оберіть тему:
<select value={theme} onChange={(event) => setTheme(event.target.value)}>
<option value="light">Світла</option>
<option value="dark">Темна</option>
</select>
</label>
<p>Поточна тема: {theme}</p>
</section>
);
}
export default function App() {
return (
<main>
<h1>Приклад Custom Hook</h1>
<Counter />
<ThemeSelector />
</main>
);
}У цьому прикладі useLocalStorage використовується двічі:
const [count, setCount] = useLocalStorage("counter-value", 0);
const [theme, setTheme] = useLocalStorage("selected-theme", "light");Кожен виклик має:
власний стан;
власний ключ у localStorage;
власну синхронізацію з браузерним сховищем.
Сам компонент Counter не знає, як саме працює localStorage. Він працює так, ніби використовує звичайний useState.
useLocalStorageconst [value, setValue] = useState(() => {
const storedValue = window.localStorage.getItem(key);
// ...
});Функція передається в useState, тому вона виконується лише для визначення початкового стану.
Якщо значення для ключа вже є в localStorage, Hook читає його та перетворює з JSON:
return JSON.parse(storedValue);Якщо значення відсутнє, використовується initialValue.
useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);Після зміни value React запускає Effect і записує нове значення в localStorage.
JSON.stringify потрібен тому, що localStorage зберігає лише рядки. Завдяки JSON Hook може працювати не тільки з рядками, а й з числами, масивами та об'єктами.
return [value, setValue];Hook повертає масив у такому самому форматі, як useState. Тому його можна використовувати аналогічно:
const [value, setValue] = useLocalStorage("some-key", "initial value");Таке API робить Custom Hook простим для заміни або повторного використання.
Custom Hooks підпорядковуються тим самим правилам, що й вбудовані Hooks.
Не викликайте Custom Hook всередині умов:
function Component({ isEnabled }) {
if (isEnabled) {
const value = useLocalStorage("key", "");
}
return null;
}Так робити не можна, оскільки порядок викликів Hooks може змінюватися між рендерами.
Правильно:
function Component({ isEnabled }) {
const [value, setValue] = useLocalStorage("key", "");
if (!isEnabled) {
return null;
}
return <p>{value}</p>;
}function Component() {
const values = items.map((item) => {
return useCustomHook(item.id);
});
return null;
}Кількість і порядок викликів Hooks не повинні залежати від кількості елементів у масиві.
Правильні місця:
function Component() {
const value = useCustomHook();
return <p>{value}</p>;
}
function useAnotherHook() {
const value = useCustomHook();
return value;
}Не слід викликати Hook зі звичайної функції, обробника події або callback-функції:
function handleClick() {
const value = useCustomHook();
}Компонент повертає JSX:
function UserInfo() {
return <p>Інформація про користувача</p>;
}Custom Hook повертає дані, функції або об'єкт:
function useUserInfo() {
return {
name: "Анна",
isAuthenticated: true,
};
}Їхнє призначення різне:
компонент відповідає за інтерфейс;
Custom Hook відповідає за повторне використання логіки.
Custom Hook не можна використовувати як JSX-компонент:
<useLocalStorage />Його потрібно викликати як функцію в тілі компонента:
const [value, setValue] = useLocalStorage("key", "");Винесення в Custom Hook доречне, якщо:
однаковий код повторюється в кількох компонентах;
код містить useState, useEffect або інші Hooks;
компонент містить занадто багато логіки, не пов'язаної безпосередньо з розміткою;
логіку можна описати окремою відповідальністю.
Наприклад, повторювану логіку можна винести для:
синхронізації з localStorage;
роботи з формами;
підписки на події браузера;
завантаження даних;
керування станом розгорнутого або вибраного елемента.
Водночас не кожну функцію потрібно перетворювати на Custom Hook. Якщо функція не використовує Hooks і не залежить від життєвого циклу компонента, це може бути звичайна JavaScript-функція.
usefunction localStorageState() {
const [value, setValue] = useState("");
return [value, setValue];
}Така назва порушує домовленість React і може завадити інструментам перевірки правил Hooks.
Правильно:
function useLocalStorageState() {
const [value, setValue] = useState("");
return [value, setValue];
}function useCounter() {
const [count, setCount] = useState(0);
return [count, setCount];
}Якщо два компоненти викличуть useCounter, вони отримають два незалежні стани. Custom Hook не робить стан глобальним.
Для спільного стану потрібен інший підхід, наприклад стан у спільному батьківському компоненті. Це не є завданням Custom Hook.
Custom Hook не повинен повертати готову розмітку замість логічного API:
function useCounter() {
// Логіку краще повертати як дані та функції
return <button>Кнопка</button>;
}Краще повернути стан і обробники:
function useCounter() {
const [count, setCount] = useState(0);
return {
count,
increment: () => setCount((value) => value + 1),
};
}Так компонент сам вирішує, як відобразити ці дані.
Hook на кшталт useEverything складно тестувати та повторно використовувати. Краще створювати Hooks із чіткою відповідальністю:
useLocalStorage працює зі збереженням значень;
useForm керує станом форми;
useDocumentTitle змінює заголовок документа.
Custom Hook — це функція для повторного використання логіки, пов'язаної з React Hooks.
Назва Custom Hook повинна починатися з use.
Custom Hook може використовувати вбудовані Hooks та інші Custom Hooks.
Hook не повертає JSX, а повертає дані, стан і функції.
Кожен виклик Custom Hook має власний незалежний стан.
Custom Hooks потрібно викликати лише на верхньому рівні компонентів або інших Hooks.
Custom Hook допомагає розділити логіку та розмітку й уникнути дублювання коду.