Создание кастомных хуков в React на практике
Содержание
- Введение: что такое кастомные хуки
- Первый кастомный хук —
useCounter useLocalStorage— синхронизация с localStorageuseFetch— загрузка данных с сервераuseDebounce— задержка перед выполнениемuseWindowSize— отслеживание размеров окнаuseMediaQuery— медиа-запросы в React- Комбинирование хуков
- Структура кастомного хука (шаблон)
- Частые ошибки новичков
- Итог (что мы узнали)
- Задание для закрепления (на 1–2 часа)
- Что дальше?
Цель урока: научиться создавать собственные хуки для выноса повторяющейся логики из компонентов. Понять, как абстрагировать сложную логику в переиспользуемые функции и сделать код чище, модульнее и профессиональнее.
Введение: что такое кастомные хуки
use и которая может использовать другие хуки внутри себя.Главная мысль:
Кастомные хуки — это способ вынести логику из компонентов в отдельные функции, чтобы переиспользовать её в разных местах.
Зачем это нужно:
- Переиспользование — одна логика в разных компонентах
- Чистота — компоненты становятся проще и читаемее
- Тестирование — легко тестировать логику отдельно
- Организация — логика разбита на маленькие части
Правила кастомных хуков:
- Название начинается с
use(например,useFetch,useLocalStorage) - Использует другие хуки внутри (
useState,useEffectи т.д.) - Возвращает данные и/или функции
Первый кастомный хук — useCounter
Простейший пример — вынос логики счётчика.
Без хука (повторяющийся код):
function ComponentA() {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const reset = () => setCount(0);
return (
<div>
<p>{count}</p>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<button onClick={reset}>Сброс</button>
</div>
);
}
function ComponentB() {
// Тот же самый код повторяется!
const [count, setCount] = useState(0);
// ... и так далее
}
С кастомным хуком:
// hooks/useCounter.js
import { useState } from 'react';
function useCounter(initialValue = 0, step = 1) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(prev => prev + step);
const decrement = () => setCount(prev => prev - step);
const reset = () => setCount(initialValue);
return { count, increment, decrement, reset };
}
// Использование
function ComponentA() {
const { count, increment, decrement, reset } = useCounter(0, 1);
return (
<div>
<p>{count}</p>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<button onClick={reset}>Сброс</button>
</div>
);
}
function ComponentB() {
const { count, increment, decrement, reset } = useCounter(10, 5);
// Используем тот же хук с другими параметрами!
}
useLocalStorage — синхронизация с localStorage
Реализация:
// hooks/useLocalStorage.js
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// Получаем значение из localStorage при инициализации
const [storedValue, setStoredValue] = useState(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
// Сохраняем в localStorage при изменении значения
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.error(error);
}
}, [key, storedValue]);
return [storedValue, setStoredValue];
}
// Использование
function UserSettings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [language, setLanguage] = useLocalStorage('language', 'ru');
return (
<div>
<p>Тема: {theme}</p>
<button onClick={() => setTheme('dark')}>Тёмная</button>
<button onClick={() => setTheme('light')}>Светлая</button>
<p>Язык: {language}</p>
<button onClick={() => setLanguage('ru')}>Русский</button>
<button onClick={() => setLanguage('en')}>English</button>
</div>
);
}
Преимущества:
- Настройки сохраняются после перезагрузки страницы
- Можно использовать в нескольких компонентах
- Обработка ошибок при работе с localStorage
useFetch — загрузка данных с сервера
Реализация:
// hooks/useFetch.js
import { useState, useEffect } from 'react';
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`Ошибка: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]); // Перезагружаем при изменении URL
return { data, loading, error };
}
// Использование
function UsersList() {
const { data: users, loading, error } = useFetch('https://api.example.com/users');
if (loading) return <p>Загрузка пользователей...</p>;
if (error) return <p>Ошибка: {error}</p>;
if (!users) return <p>Нет данных</p>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
function ProductsList() {
const { data: products, loading, error } = useFetch('https://api.example.com/products');
// Та же логика, но для других данных!
}
Оживи свой сайт. Освой JavaScript!
Статичная верстка — это только скелет. Наш онлайн-курс "JavaScript с нуля до профи" даст твоим страницам мышцы и нервы. Научись создавать слайдеры, формы, интерактивные карты и получать данные с сервера.
От теории — к реальным скриптам в твоём портфолио.
useDebounce — задержка перед выполнением
Реализация:
// hooks/useDebounce.js
import { useState, useEffect } from 'react';
function useDebounce(value, delay = 500) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// Использование — поиск с задержкой
function SearchBar() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearch = useDebounce(searchTerm, 500);
useEffect(() => {
if (debouncedSearch) {
console.log(`Поиск: ${debouncedSearch}`);
// Отправляем запрос на сервер
fetchSearchResults(debouncedSearch);
}
}, [debouncedSearch]);
return (
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Поиск..."
/>
);
}
Что даёт debounce:
- Запрос отправляется только после того, как пользователь перестал печатать
- Экономия ресурсов сервера
- Меньше лишних запросов
useWindowSize — отслеживание размеров окна
Реализация:
// hooks/useWindowSize.js
import { useState, useEffect } from 'react';
function useWindowSize() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowSize;
}
// Использование
function ResponsiveComponent() {
const { width } = useWindowSize();
return (
<div>
<p>Ширина окна: {width}px</p>
{width < 768 ? (
<p>Мобильная версия</p>
) : width < 1024 ? (
<p>Планшетная версия</p>
) : (
<p>Десктопная версия</p>
)}
</div>
);
}
useMediaQuery — медиа-запросы в React
Реализация:
// hooks/useMediaQuery.js
import { useState, useEffect } from 'react';
function useMediaQuery(query) {
const [matches, setMatches] = useState(() => {
if (typeof window !== 'undefined') {
return window.matchMedia(query).matches;
}
return false;
});
useEffect(() => {
const mediaQuery = window.matchMedia(query);
const handler = (event) => {
setMatches(event.matches);
};
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}, [query]);
return matches;
}
// Использование
function AdaptiveComponent() {
const isMobile = useMediaQuery('(max-width: 768px)');
const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)');
const isDesktop = useMediaQuery('(min-width: 1025px)');
return (
<div>
{isMobile && <MobileMenu />}
{isTablet && <TabletMenu />}
{isDesktop && <DesktopMenu />}
</div>
);
}
Комбинирование хуков
Кастомные хуки могут использовать другие кастомные хуки:
// hooks/useUserProfile.js
import useFetch from './useFetch';
import useLocalStorage from './useLocalStorage';
function useUserProfile(userId) {
const [token] = useLocalStorage('authToken', null);
const { data: user, loading, error } = useFetch(
`https://api.example.com/users/${userId}`,
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
return { user, loading, error };
}
// Использование
function ProfilePage() {
const { user, loading, error } = useUserProfile(123);
if (loading) return <p>Загрузка...</p>;
if (error) return <p>Ошибка: {error}</p>;
if (!user) return <p>Пользователь не найден</p>;
return <div>{user.name}</div>;
}
Структура кастомного хука (шаблон)
// hooks/useCustomHook.js
import { useState, useEffect, useCallback } from 'react';
function useCustomHook(param1, param2) {
// 1. Состояния
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// 2. Вспомогательные функции
const processData = useCallback((rawData) => {
// Обработка данных
return rawData;
}, []);
// 3. Эффекты
useEffect(() => {
// Логика при монтировании или изменении параметров
}, [param1, param2]);
// 4. Возвращаемые значения
return {
data,
loading,
error,
processData,
};
}
export default useCustomHook;
Частые ошибки новичков
| Ошибка | Почему | Исправление |
|---|---|---|
Не начинается с use |
Реакт не видит, что это хук | Всегда начинай с use |
| Вызов хука внутри условия | Нарушает правила хуков | Вызывай на верхнем уровне |
| Избыточное создание хуков | Раздувание кода | Создавай только для повторяющейся логики |
| Хук возвращает слишком много | Сложно использовать | Дели на несколько хуков |
| Не использует другие хуки | Это просто функция, а не хук | Используй хуки внутри |
Итог (что мы узнали)
| Что узнали | Как это работает |
|---|---|
| Кастомные хуки | Функции с use для выноса логики |
useCounter |
Счётчик с логикой |
useLocalStorage |
Синхронизация с localStorage |
useFetch |
Загрузка данных с сервера |
useDebounce |
Задержка перед выполнением |
useWindowSize |
Отслеживание размеров окна |
useMediaQuery |
Медиа-запросы в React |
| Комбинирование | Хуки внутри хуков |
Задание для закрепления (на 1–2 часа)
Создай три кастомных хука:
useToggle— переключение состояния (true/false) с методами:toggle,setTrue,setFalseuseArray— работа с массивом методами:push,remove,update,clear,sortuseForm— управление формой с валидацией: принимает начальные значения и правила валидации, возвращает поля (values), обработчики (handleChange,handleSubmit), ошибки (errors), валидация наblur
Дополнительно: Используй все три хука в одном компоненте: useForm для формы регистрации, useToggle для показа/скрытия пароля, useArray для списка добавленных пользователей.
Проверка: Убедись, что все хуки работают корректно и могут быть переиспользованы в других компонентах.
Что дальше?
В следующем уроке разберём Context API — мощный инструмент для передачи данных между компонентами без prop drilling. Ты научишься создавать глобальные состояния для темы, авторизации и настроек приложения, избегая передачи пропсов через много уровней.