Как реализовать собственный хук (custom hook) и в каких случаях это нужно?

В React custom hook (пользовательский хук) — это функция, имя которой начинается с use, и которая использует внутри себя другие хуки (useState, useEffect, useRef, useMemo и т.д.). Они позволяют выносить повторяющуюся логику из компонентов в переиспользуемые функции, сохраняя при этом доступ к жизненному циклу компонента и реактивности.

Когда нужно создавать custom hook

  1. Повторяющаяся логика в разных компонентах
    Например, одинаковая логика для подписки на события, работы с формами, API-запросами, debounce и т.п.

  2. Абстрагирование сложной логики
    Вынос сложных и плохо читаемых блоков useEffect, useState, таймеров и т.д. в изолированные функции.

  3. Упрощение и декомпозиция компонента
    Чтобы основной компонент не был перегружен логикой, а лишь отображал результат.

  4. Инкапсуляция доступа к внешним данным или API
    Например, useUser, useProducts, useAuth, useTheme и т.д.

Как создать кастомный хук — базовая структура

import { useState, useEffect } from 'react';
function useWindowWidth() {
const \[width, setWidth\] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, \[\]);
return width;
}

Использование в компоненте:

function MyComponent() {
const width = useWindowWidth();
return <p>Ширина окна: {width}px</p>;
}

Примеры полезных custom hooks

1. usePrevious — для хранения предыдущего значения

import { useRef, useEffect } from 'react';
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, \[value\]);
return ref.current;
}

2. useDebounce — отложенное обновление значения

import { useState, useEffect } from 'react';
function useDebounce(value, delay = 500) {
const \[debouncedValue, setDebouncedValue\] = useState(value);
useEffect(() => {
const id = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(id);
}, \[value, delay\]);
return debouncedValue;
}

Использование:

const searchTerm = useDebounce(inputValue, 300);

3. useLocalStorage — хук для работы с localStorage

import { useState } from 'react';
function useLocalStorage(key, initialValue) {
const \[value, setValue\] = useState(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setStoredValue = (newValue) => {
try {
const valueToStore = newValue instanceof Function ? newValue(value) : newValue;
setValue(valueToStore);
localStorage.setItem(key, JSON.stringify(valueToStore));
} catch {}
};
return \[value, setStoredValue\];
}

Использование:

const \[theme, setTheme\] = useLocalStorage('theme', 'light');

Особенности и правила custom хуков

  • Имена всегда начинаются с use — это нужно, чтобы React распознал функцию как хук и применил правила работы с хуками.

  • Хуки можно вызывать только на верхнем уровне функции — нельзя вызывать внутри if, for, try/catch, вложенных функций и т.д.

  • Custom hook может возвращать что угодно — массив, объект, примитивы. Главное — контракт между автором и потребителем.

Пример сложного хука: useFetch

import { useState, useEffect } from 'react';
function useFetch(url, options = {}) {
const \[data, setData\] = useState(null);
const \[error, setError\] = useState();
const \[loading, setLoading\] = useState(true);
useEffect(() => {
let isMounted = true;
setLoading(true);
fetch(url, options)
.then((res) => {
if (!res.ok) throw new Error('Ошибка запроса');
return res.json();
})
.then((data) => {
if (isMounted) setData(data);
})
.catch((err) => {
if (isMounted) setError(err);
})
.finally(() => {
if (isMounted) setLoading(false);
});
return () => {
isMounted = false;
};
}, \[url\]);
return { data, error, loading };
}

Использование:

const { data, error, loading } = useFetch('/api/products');

Комбинирование хуков внутри custom hook

В одном custom hook можно использовать любые другие хуки — useState, useEffect, useRef, useMemo, useCallback, а также другие custom hooks.

function useCustomThing() {
const value = useLocalStorage('key', 'default');
const debounced = useDebounce(value, 300);
return debounced;
}

Где хранить custom hooks

  • В каталоге hooks/ или src/hooks

  • Один хук — один файл: useScrollPosition.js, useDarkMode.ts, useClipboard.js

  • Если хук связан с доменом (например, useCart в интернет-магазине), можно хранить в features/cart/hooks/

Польза custom hooks

  • Повышают читаемость компонентов

  • Обеспечивают повторное использование логики

  • Упрощают тестирование (тестируются отдельно от UI)

  • Делают проект более масштабируемым и поддерживаемым

Возможности TypeScript с custom hooks

Типизация хуков позволяет чётко описывать входные и выходные данные:

function useCounter(initial: number = 0): \[number, () => void\] {
const \[count, setCount\] = useState<number>(initial);
const increment = () => setCount(c => c + 1);
return \[count, increment\];
}

Нейминг

  • useForm, useAuth, useTimer, useDarkMode, usePagination, useIntersectionObserver

  • Название должно точно отражать логику или поведение хука

  • Если используется несколько хуков — начинай с глагола: useFetchPosts, useToggleModal, useHandleScroll

Custom hooks — это мощный инструмент React, позволяющий создавать чистую, переиспользуемую и модульную логику приложения без зависимости от UI-компонентов.