Введение в 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 для переиспользования логики.
Правило хуков: вызывайте хуки только на верхнем уровне компонента, не в условиях или циклах.
