Як створити власний хук?
Кроки створення кастомного хука
- Дай ім'я з
useНапр.:useLocalStorage,useDebouncedValue,useEventListener. - Пиши як звичайну функцію
Всередині можеш викликати інші хуки (
useState,useEffect, ...). - Дотримуйся правил хуків
Тільки верхній рівень (не в
if/for), тільки всередині React-функцій. - Визнач зрозумілий API
Повертай те, що потрібно споживачу: значення, сеттери, статуси (
loading,error), методи. - Правильно вкажи залежності
У
useEffect/useMemo/useCallbackне забувай повний список залежностей.
Шаблон 1: базовий хук стану з побічним ефектом ("Hello, custom hook!")
useCounter.ts
javascript
import { useCallback, useState } from "react";
export function useCounter(initial = 0, step = 1) {
const [count, setCount] = useState(initial);
const inc = useCallback(() => setCount(c => c + step), [step]);
const dec = useCallback(() => setCount(c => c - step), [step]);
const reset = useCallback(() => setCount(initial), [initial]);
return { count, inc, dec, reset };
}Використання
javascript
function Counter() {
const { count, inc, dec, reset } = useCounter(10, 2);
return (
<>
<p>{count}</p>
<button onClick={dec}>-</button>
<button onClick={inc}>+</button>
<button onClick={reset}>Reset</button>
</>
);
}Шаблон 2: "прикладний" - локальне сховище (persist)
useLocalStorage.ts
javascript
import { useEffect, useState } from "react";
export function useLocalStorage<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
try {
const raw = localStorage.getItem(key);
return raw != null ? JSON.parse(raw) as T : initial;
} catch {
return initial;
}
});
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch {
// ігноруємо квоту/приватний режим
}
}, [key, value]);
return [value, setValue] as const;
}Використання
javascript
const [theme, setTheme] = useLocalStorage<'light'|'dark'>('theme', 'light');Шаблон 3: оптимізація вводу - "дебаунс" значення
useDebouncedValue.ts
javascript
import { useEffect, useState } from "react";
export function useDebouncedValue<T>(value: T, delay = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
}Використання (пошук без лагів)
javascript
function Search({ items }: { items: string[] }) {
const [q, setQ] = useState("");
const dq = useDebouncedValue(q, 350);
const filtered = useMemo(() => items.filter(i => i.includes(dq)), [items, dq]);
return (
<>
<input value={q} onChange={e => setQ(e.target.value)} />
<ul>{filtered.map((x, i) => <li key={i}>{x}</li>)}</ul>
</>
);
}Патерни та поради
- Повертай кортеж або об'єкт.
Кортеж (
[value, setValue]) - якщо семантика як уuseState. Об'єкт - якщо повертаєш багато полів/методів. - Стабілізуй функції (
useCallback) і обчислення (useMemo), якщо хук віддає їх назовні і це впливає на ререндери споживача. - Не роби мережеві запити в
useLayoutEffect- звичайнийuseEffectпідійде; кеш/скасування - черезAbortController. - Покривай "кутові випадки".
Обробляй помилки, SSR (перевіряй наявність
window), відписки в cleanup. - Лінтинг: підключи
eslint-plugin-react-hooksі не ігноруйexhaustive-deps. - Тестування:
для юніт-тестів використовуй
@testing-library/react+renderHookз@testing-library/react-hooks/@testing-library/react.
Коротка відповідь
Для співбесідиPremium
Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.