Назад к блогу
Frontend

React Hooks: полное руководство от useState до custom hooks

Глубокое погружение в React Hooks: useState, useEffect, useContext, useMemo, useCallback, useRef и создание собственных хуков.

5 января 2026 г.
14 мин чтения
73 просмотров
MOLOTILO

MOLOTILO DIGITAL

React Hooks: полное руководство от useState до custom hooks

Введение в React Hooks

React Hooks появились в версии 16.8 и изменили способ написания компонентов. Хуки позволяют использовать состояние и другие возможности React без классов. Рассмотрим все основные хуки и паттерны их использования.

useState — управление состоянием

useState — базовый хук для локального состояния компонента:

import { useState } from 'react';

// Простое состояние
function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <button onClick={() => setCount(count + 1)}>
      Clicked {count} times
    </button>
  );
}

// Состояние с объектом
interface User {
  name: string;
  email: string;
  age: number;
}

function UserForm() {
  const [user, setUser] = useState<User>({
    name: '',
    email: '',
    age: 0
  });
  
  // Обновление одного поля
  const updateField = (field: keyof User, value: string | number) => {
    setUser(prev => ({ ...prev, [field]: value }));
  };
  
  return (
    <form>
      <input
        value={user.name}
        onChange={e => updateField('name', e.target.value)}
      />
    </form>
  );
}

// Ленивая инициализация (для тяжёлых вычислений)
function ExpensiveComponent() {
  const [data, setData] = useState(() => {
    // Выполняется только при первом рендере
    return computeExpensiveValue();
  });
}

useEffect — побочные эффекты

useEffect выполняет побочные эффекты: запросы к API, подписки, манипуляции с DOM:

import { useState, useEffect } from 'react';

function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    // Флаг для отмены при размонтировании
    let cancelled = false;
    
    async function fetchUser() {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        const data = await response.json();
        
        if (!cancelled) {
          setUser(data);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err as Error);
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    }
    
    fetchUser();
    
    // Cleanup функция
    return () => {
      cancelled = true;
    };
  }, [userId]); // Зависимость — перезапуск при изменении userId

  if (loading) return <Spinner />;
  if (error) return <Error message={error.message} />;
  return <UserCard user={user} />;
}

// Подписка на события
function WindowSize() {
  const [size, setSize] = useState({ width: 0, height: 0 });

  useEffect(() => {
    function handleResize() {
      setSize({ width: window.innerWidth, height: window.innerHeight });
    }
    
    handleResize(); // Начальное значение
    window.addEventListener('resize', handleResize);
    
    return () => window.removeEventListener('resize', handleResize);
  }, []); // Пустой массив — только при монтировании

  return <div>{size.width} x {size.height}</div>;
}

useContext — глобальное состояние

import { createContext, useContext, useState, ReactNode } from 'react';

// Создание контекста
interface ThemeContextType {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextType | null>(null);

// Провайдер
function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');
  
  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };
  
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// Кастомный хук для использования контекста
function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

// Использование
function ThemeToggle() {
  const { theme, toggleTheme } = useTheme();
  
  return (
    <button onClick={toggleTheme}>
      Current: {theme}
    </button>
  );
}

useMemo и useCallback — оптимизация

import { useMemo, useCallback, useState } from 'react';

function ProductList({ products, filter }: Props) {
  // useMemo — мемоизация вычислений
  const filteredProducts = useMemo(() => {
    console.log('Filtering products...');
    return products.filter(p => 
      p.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [products, filter]); // Пересчёт только при изменении зависимостей

  // useCallback — мемоизация функций
  const handleClick = useCallback((id: string) => {
    console.log('Clicked:', id);
  }, []); // Функция не пересоздаётся

  return (
    <ul>
      {filteredProducts.map(product => (
        <ProductItem 
          key={product.id} 
          product={product}
          onClick={handleClick}
        />
      ))}
    </ul>
  );
}

// Когда использовать:
// useMemo — тяжёлые вычисления, сложные фильтрации
// useCallback — передача функций в мемоизированные дочерние компоненты

useRef — ссылки и мутабельные значения

import { useRef, useEffect } from 'react';

// Ссылка на DOM элемент
function TextInput() {
  const inputRef = useRef<HTMLInputElement>(null);
  
  useEffect(() => {
    inputRef.current?.focus();
  }, []);
  
  return <input ref={inputRef} />;
}

// Хранение предыдущего значения
function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T>();
  
  useEffect(() => {
    ref.current = value;
  }, [value]);
  
  return ref.current;
}

// Хранение мутабельного значения без ререндера
function Timer() {
  const intervalRef = useRef<NodeJS.Timeout>();
  
  const start = () => {
    intervalRef.current = setInterval(() => {
      console.log('tick');
    }, 1000);
  };
  
  const stop = () => {
    clearInterval(intervalRef.current);
  };
  
  return (
    <>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </>
  );
}

Custom Hooks — переиспользование логики

// useLocalStorage
function useLocalStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = (value: T | ((val: T) => T)) => {
    const valueToStore = value instanceof Function ? value(storedValue) : value;
    setStoredValue(valueToStore);
    window.localStorage.setItem(key, JSON.stringify(valueToStore));
  };

  return [storedValue, setValue] as const;
}

// useFetch
function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    const controller = new AbortController();
    
    fetch(url, { signal: controller.signal })
      .then(res => res.json())
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
    
    return () => controller.abort();
  }, [url]);

  return { data, loading, error };
}

// useDebounce
function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// Использование
function SearchComponent() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 300);
  const { data, loading } = useFetch(`/api/search?q=${debouncedQuery}`);
  
  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      {loading ? <Spinner /> : <Results data={data} />}
    </div>
  );
}

Заключение

React Hooks — мощный инструмент для управления состоянием и побочными эффектами. Начните с useState и useEffect, затем изучите оптимизацию с useMemo/useCallback. Создавайте custom hooks для переиспользования логики.

Правило хуков: вызывайте хуки только на верхнем уровне компонента, не в условиях или циклах.

ReactHooksuseStateuseEffectFrontend

Понравилась статья?

Подпишитесь на наш блог, чтобы не пропустить новые материалы