Хуки — вся мощь React в одной коробке
Содержание
- Введение: что такое хуки и зачем они нужны
useState— управление состояниемuseEffect— управление побочными эффектамиuseMemo— мемоизация вычисленийuseReducer— сложное управление состояниемuseCallback— мемоизация функцийuseRef— работа с DOM и хранение без перерендераuseContext— доступ к глобальным данным- Сравнительная таблица всех хуков
- Частые ошибки новичков
- Итог (что мы узнали)
- Задание для закрепления (на 2–3 часа)
- Что дальше?
Цель урока: познакомиться со всеми базовыми хуками React, которые нужны в повседневной разработке. Научиться использовать каждый хук по назначению: от управления состоянием до оптимизации производительности и работы с DOM.
Введение: что такое хуки и зачем они нужны
Хуки (Hooks) — это функции, которые позволяют «подключаться» к состоянию и возможностям React из функциональных компонентов.
Зачем они нужны?
- Появились в React 16.8 (2019 год)
- Позволяют использовать
stateи жизненный цикл без классов - Делают код проще, чище и переиспользуемее
Главная мысль:
Хуки — это новый способ писать React-компоненты. Они заменяют классовые компоненты во всех случаях, кроме Error Boundaries.
Основные правила хуков:
- Вызывай хуки только на верхнем уровне (не внутри циклов, условий или вложенных функций)
- Вызывай хуки только внутри функциональных компонентов или кастомных хуков
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>
);
}
Когда использовать:
- Сложные вычисления (фильтрация, сортировка больших массивов)
- Вычисления, которые зависят от тяжёлых операций
- Когда компонент часто перерендеривается
Когда НЕ использовать:
- Простые вычисления (например, сложение)
- Вычисления, которые не зависят от пропсов/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>
);
}
Когда использовать:
- Сложная логика состояния (несколько взаимосвязанных полей)
- Состояние зависит от предыдущих значений
- Множество способов изменения состояния
- Лучше читается, чем множество 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>
);
}
Когда использовать:
- Функции, которые передаются дочерним компонентам (особенно с
memo) - Функции, которые используются в
useEffectкак зависимости - Функции, которые используются в
useMemo
Когда НЕ использовать:
- Для всех функций без разбора (это преждевременная оптимизация)
- Простые функции, которые не вызывают проблем с производительностью
useRef — работа с DOM и хранение без перерендера
Для чего:
- Получать доступ к DOM-элементам напрямую
- Хранить значения, которые не вызывают перерендер при изменении
Синтаксис:
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 уровня
- Данные, которые часто меняются (в этом случае лучше 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 часа)
Создай приложение «Корзина покупок», используя все изученные хуки:
useState— хранение списка товаров и корзиныuseReducer— управление корзиной (добавить, удалить, изменить количество, очистить)useMemo— вычисление общей суммы корзиныuseCallback— функции добавления/удаления для дочерних компонентовuseRef— фокус на поле поиска при монтированииuseContext— тема приложения (светлая/тёмная)useEffect— сохранение корзины вlocalStorageпри изменении
Дополнительно: добавь отображение количества товаров в корзине.
Проверка: проверь все функциональности: добавление, удаление, изменение количества, подсчёт суммы, сохранение в localStorage.
Что дальше?
В следующем уроке разберём Написание своих хуков — как создавать собственные переиспользуемые хуки для выноса логики. Ты научишься абстрагировать повторяющийся код и создавать чистые, удобные для тестирования хуки. Это сделает твой код более модульным и профессиональным.