Хотите заказать веб-сайт? Связаться с нами

Создание кастомных хуков в React на практике

Цель урока: научиться создавать собственные хуки для выноса повторяющейся логики из компонентов. Понять, как абстрагировать сложную логику в переиспользуемые функции и сделать код чище, модульнее и профессиональнее.

Создание кастомных хуков в React на практике

Введение: что такое кастомные хуки

Кастомный хук — это обычная JavaScript-функция, название которой начинается с use и которая может использовать другие хуки внутри себя.

Главная мысль:

Кастомные хуки — это способ вынести логику из компонентов в отдельные функции, чтобы переиспользовать её в разных местах.

Зачем это нужно:

  1. Переиспользование — одна логика в разных компонентах
  2. Чистота — компоненты становятся проще и читаемее
  3. Тестирование — легко тестировать логику отдельно
  4. Организация — логика разбита на маленькие части

Правила кастомных хуков:

  1. Название начинается с use (например, useFetch, useLocalStorage)
  2. Использует другие хуки внутри (useState, useEffect и т.д.)
  3. Возвращает данные и/или функции

Первый кастомный хук — 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>
  );
}

Преимущества:

  1. Настройки сохраняются после перезагрузки страницы
  2. Можно использовать в нескольких компонентах
  3. Обработка ошибок при работе с 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:

  1. Запрос отправляется только после того, как пользователь перестал печатать
  2. Экономия ресурсов сервера
  3. Меньше лишних запросов

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 часа)

Создай три кастомных хука:

  1. useToggle — переключение состояния (true/false) с методами: toggle, setTrue, setFalse
  2. useArray — работа с массивом методами: push, remove, update, clear, sort
  3. useForm — управление формой с валидацией: принимает начальные значения и правила валидации, возвращает поля (values), обработчики (handleChange, handleSubmit), ошибки (errors), валидация на blur

Дополнительно: Используй все три хука в одном компоненте: useForm для формы регистрации, useToggle для показа/скрытия пароля, useArray для списка добавленных пользователей.

Проверка: Убедись, что все хуки работают корректно и могут быть переиспользованы в других компонентах.

Что дальше?

В следующем уроке разберём Context API — мощный инструмент для передачи данных между компонентами без prop drilling. Ты научишься создавать глобальные состояния для темы, авторизации и настроек приложения, избегая передачи пропсов через много уровней.

Теги: