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

Хуки — вся мощь React в одной коробке

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

Хуки — вся мощь React в одной коробке

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

Хуки (Hooks) — это функции, которые позволяют «подключаться» к состоянию и возможностям React из функциональных компонентов.

Зачем они нужны?

  1. Появились в React 16.8 (2019 год)
  2. Позволяют использовать state и жизненный цикл без классов
  3. Делают код проще, чище и переиспользуемее

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

Хуки — это новый способ писать React-компоненты. Они заменяют классовые компоненты во всех случаях, кроме Error Boundaries.

Основные правила хуков:

  1. Вызывай хуки только на верхнем уровне (не внутри циклов, условий или вложенных функций)
  2. Вызывай хуки только внутри функциональных компонентов или кастомных хуков

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

Для чего: хранить и изменять данные внутри компонента.

Синтаксис:

const [state, setState] = useState(initialValue);

Пример:

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  return (
    <div>
      <p>Счёт: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      
      <input 
        value={name} 
        onChange={(e) => setName(e.target.value)} 
        placeholder="Введите имя"
      />
      <p>Привет, {name || 'Гость'}!</p>
    </div>
  );
}

Типы state:

const [count, setCount] = useState(0);           // число
const [name, setName] = useState('');            // строка
const [isActive, setIsActive] = useState(false); // булево
const [items, setItems] = useState([]);          // массив
const [user, setUser] = useState(null);          // объект или null

useEffect — управление побочными эффектами

Для чего: выполнять код при монтировании, обновлении и размонтировании компонента.

Синтаксис:

useEffect(() => {
  // Эффект
  return () => {
    // Очистка
  };
}, [dependencies]);

Примеры:

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  // 1. Загрузка данных при монтировании
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      });
  }, []); // Пустой массив — только при монтировании

  // 2. Реакция на изменение userId
  useEffect(() => {
    console.log(`UserId изменился на ${userId}`);
  }, [userId]); // Выполняется при изменении userId

  // 3. Таймер с очисткой
  useEffect(() => {
    const interval = setInterval(() => {
      console.log('Tick');
    }, 1000);

    return () => clearInterval(interval); // Очистка при размонтировании
  }, []);

  if (loading) return <p>Загрузка...</p>;
  return <div>{user.name}</div>;
}

useMemo — мемоизация вычислений

Для чего: кешировать результат сложных вычислений, чтобы не пересчитывать их при каждом рендере.

Синтаксис:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

Пример:

import { useState, useMemo } from 'react';

function ProductList({ products, filter }) {
  const [sortOrder, setSortOrder] = useState('asc');

  // Без useMemo — будет пересчитываться при каждом рендере
  // С useMemo — только когда меняются products или filter
  const filteredAndSortedProducts = useMemo(() => {
    console.log('Пересчёт списка...'); // Увидишь только при изменении зависимостей
    
    // Фильтрация
    let result = products.filter(p => 
      p.name.toLowerCase().includes(filter.toLowerCase())
    );
    
    // Сортировка
    result = result.sort((a, b) => {
      return sortOrder === 'asc' 
        ? a.price - b.price 
        : b.price - a.price;
    });
    
    return result;
  }, [products, filter, sortOrder]); // Зависимости

  return (
    <div>
      <button onClick={() => setSortOrder('asc')}>По возрастанию</button>
      <button onClick={() => setSortOrder('desc')}>По убыванию</button>
      
      {filteredAndSortedProducts.map(p => (
        <div key={p.id}>{p.name} - {p.price}₽</div>
      ))}
    </div>
  );
}

Когда использовать:

  1. Сложные вычисления (фильтрация, сортировка больших массивов)
  2. Вычисления, которые зависят от тяжёлых операций
  3. Когда компонент часто перерендеривается

Когда НЕ использовать:

  1. Простые вычисления (например, сложение)
  2. Вычисления, которые не зависят от пропсов/state
Из ноутбука появляются летающие светящиеся экраны

Оживи свой сайт. Освой JavaScript!

Статичная верстка — это только скелет. Наш онлайн-курс "JavaScript с нуля до профи" даст твоим страницам мышцы и нервы. Научись создавать слайдеры, формы, интерактивные карты и получать данные с сервера.

От теории — к реальным скриптам в твоём портфолио.

Подробнее о курсе

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

Для чего: альтернатива useState для сложной логики состояния, когда нужно обрабатывать несколько типов действий.

Синтаксис:

const [state, dispatch] = useReducer(reducer, initialState);

Пример:

import { useReducer } from 'react';

// 1. Начальное состояние
const initialState = { count: 0, step: 1 };

// 2. Функция-редьюсер (определяет, как состояние меняется в ответ на действия)
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + state.step };
    case 'decrement':
      return { ...state, count: state.count - state.step };
    case 'setStep':
      return { ...state, step: action.payload };
    case 'reset':
      return initialState;
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>Счёт: {state.count}</p>
      <p>Шаг: {state.step}</p>
      
      <button onClick={() => dispatch({ type: 'increment' })}>+{state.step}</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-{state.step}</button>
      
      <input 
        type="number" 
        value={state.step} 
        onChange={(e) => dispatch({ 
          type: 'setStep', 
          payload: Number(e.target.value) 
        })}
      />
      
      <button onClick={() => dispatch({ type: 'reset' })}>Сброс</button>
    </div>
  );
}

Когда использовать:

  1. Сложная логика состояния (несколько взаимосвязанных полей)
  2. Состояние зависит от предыдущих значений
  3. Множество способов изменения состояния
  4. Лучше читается, чем множество useState с логикой

useCallback — мемоизация функций

Для чего: кешировать функцию между рендерами, чтобы не создавать её заново при каждом рендере.

Синтаксис:

const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

Пример:

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

// Дочерний компонент с memo (чтобы избежать лишних рендеров)
const Child = memo(({ onClick, label }) => {
  console.log(`${label} рендерится`);
  return <button onClick={onClick}>{label}</button>;
});

function Parent() {
  const [count, setCount] = useState(0);
  const [otherState, setOtherState] = useState(0);

  // Без useCallback: функция создаётся заново при каждом рендере
  // С useCallback: функция будет одной и той же, пока не изменится count
  const handleIncrement = useCallback(() => {
    setCount(prev => prev + 1);
  }, []); // Пустой массив — функция создаётся один раз

  // Если функция зависит от count
  const handleIncrementBy = useCallback(() => {
    setCount(prev => prev + count);
  }, [count]); // Новая функция при изменении count

  return (
    <div>
      <p>Счёт: {count}</p>
      <Child onClick={handleIncrement} label="+1" />
      <button onClick={() => setOtherState(prev => prev + 1)}>
        Изменить другое состояние: {otherState}
      </button>
    </div>
  );
}

Когда использовать:

  1. Функции, которые передаются дочерним компонентам (особенно с memo)
  2. Функции, которые используются в useEffect как зависимости
  3. Функции, которые используются в useMemo

Когда НЕ использовать:

  1. Для всех функций без разбора (это преждевременная оптимизация)
  2. Простые функции, которые не вызывают проблем с производительностью

useRef — работа с DOM и хранение без перерендера

Для чего:

  1. Получать доступ к DOM-элементам напрямую
  2. Хранить значения, которые не вызывают перерендер при изменении

Синтаксис:

const ref = useRef(initialValue);
// ref.current — значение

Примеры

1. Доступ к DOM-элементу:

import { useRef, useEffect } from 'react';

function AutoFocusInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    // Фокус на поле при монтировании
    inputRef.current.focus();
  }, []);

  return <input ref={inputRef} placeholder="Автофокус" />;
}

2. Хранение без перерендера:

function Stopwatch() {
  const [seconds, setSeconds] = useState(0);
  const intervalRef = useRef(null);

  const startTimer = () => {
    if (intervalRef.current) return; // Уже запущен
    
    intervalRef.current = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);
  };

  const stopTimer = () => {
    clearInterval(intervalRef.current);
    intervalRef.current = null;
  };

  return (
    <div>
      <p>Секунд: {seconds}</p>
      <button onClick={startTimer}>Старт</button>
      <button onClick={stopTimer}>Стоп</button>
    </div>
  );
}

3. Хранение предыдущего значения:

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

useContext — доступ к глобальным данным

Для чего: доступ к данным без передачи через все уровни компонентов (prop drilling).

Синтаксис:

const value = useContext(MyContext);

Пример:

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

// 1. Создаём контекст
const ThemeContext = createContext();

// 2. Провайдер в родительском компоненте
function App() {
  const [theme, setTheme] = useState('light');

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

// 3. Дочерний компонент, который использует контекст
function Toolbar() {
  const { theme, setTheme } = useContext(ThemeContext);
  
  return (
    <div style={{ background: theme === 'light' ? '#fff' : '#333' }}>
      <p>Текущая тема: {theme}</p>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Переключить тему
      </button>
    </div>
  );
}

Когда использовать:

  1. Тема приложения (светлая/тёмная)
  2. Авторизация пользователя
  3. Настройки приложения

Локализация (язык)

Когда НЕ использовать:

  1. Для простой передачи пропсов на 1-2 уровня
  2. Данные, которые часто меняются (в этом случае лучше Redux)

Сравнительная таблица всех хуков

Хук Что делает Когда использовать
useState Состояние компонента Всегда для простых данных
useEffect Побочные эффекты Загрузка данных, подписки, таймеры
useMemo Кеширование вычислений Дорогие вычисления, фильтрация
useReducer Сложное состояние Множество действий, связанные поля
useCallback Кеширование функций Передача функций в дочерние компоненты
useRef DOM-доступ и хранение без рендера Фокус, таймеры, предыдущие значения
useContext Глобальные данные Тема, пользователь, настройки

Частые ошибки новичков

Ошибка Почему Исправление
Вызов хука в условии Нарушает порядок вызовов Выноси на верхний уровень
Бесконечный useEffect Обновление state без зависимостей Добавь зависимости или проверь логику
useMemo для всего Преждевременная оптимизация Используй только для дорогих вычислений
Мутация ref напрямую Не вызывает перерендер Используй правильно (это и есть цель)
Создание контекста в компоненте Создаётся заново при каждом рендере Выноси контекст за компонент

Итог (что мы узнали)

Что узнали Как это работает
useState Управление локальным состоянием
useEffect Работа с побочными эффектами и жизненным циклом
useMemo Оптимизация вычислений
useReducer Сложное состояние с логикой
useCallback Оптимизация функций
useRef DOM-доступ и хранение данных
useContext Глобальные данные без prop drilling

Задание для закрепления (на 2–3 часа)

Создай приложение «Корзина покупок», используя все изученные хуки:

  1. useState — хранение списка товаров и корзины
  2. useReducer — управление корзиной (добавить, удалить, изменить количество, очистить)
  3. useMemo — вычисление общей суммы корзины
  4. useCallback — функции добавления/удаления для дочерних компонентов
  5. useRef — фокус на поле поиска при монтировании
  6. useContext — тема приложения (светлая/тёмная)
  7. useEffect — сохранение корзины в localStorage при изменении

Дополнительно: добавь отображение количества товаров в корзине.

Проверка: проверь все функциональности: добавление, удаление, изменение количества, подсчёт суммы, сохранение в localStorage.

Что дальше?

В следующем уроке разберём Написание своих хуков — как создавать собственные переиспользуемые хуки для выноса логики. Ты научишься абстрагировать повторяющийся код и создавать чистые, удобные для тестирования хуки. Это сделает твой код более модульным и профессиональным.

Теги: