useState
Базовое использование
Заголовок раздела «Базовое использование»import { useState } from 'react';
function Counter() { // [текущее значение, функция обновления] const [count, setCount] = useState(0);
return ( <button onClick={() => setCount(count + 1)}> Счётчик: {count} </button> );}Типизация с TypeScript
Заголовок раздела «Типизация с TypeScript»// Тип выводится автоматическиconst [count, setCount] = useState(0); // numberconst [name, setName] = useState(''); // stringconst [visible, setVisible] = useState(false); // boolean
// Явная типизация нужна для null / union / сложных объектовconst [user, setUser] = useState<User | null>(null);const [status, setStatus] = useState<'idle' | 'loading' | 'error'>('idle');
interface User { id: number; name: string; email: string;}Функциональные обновления
Заголовок раздела «Функциональные обновления»// ❌ Может пропустить обновление при нескольких вызовахsetCount(count + 1);setCount(count + 1); // оба используют одинаковый count!
// ✅ Функциональная форма — гарантированно актуальный стейтsetCount(prev => prev + 1);setCount(prev => prev + 1); // +2
// Практический пример — добавление в массивconst [items, setItems] = useState<string[]>([]);
function addItem(item: string) { setItems(prev => [...prev, item]);}
// Обновление объекта (нужен spread — setState не мержит объекты как в классах)const [user, setUser] = useState({ name: 'Иван', age: 25 });
function updateName(name: string) { setUser(prev => ({ ...prev, name }));}Ленивая инициализация
Заголовок раздела «Ленивая инициализация»// ❌ Тяжёлые вычисления выполняются на каждом рендереconst [data, setData] = useState(expensiveComputation()); // плохо!
// ✅ Функция вызывается только один раз при монтированииconst [data, setData] = useState(() => expensiveComputation());
// Пример: чтение из localStoragefunction usePersistedState<T>(key: string, defaultValue: T) { const [state, setState] = useState<T>(() => { try { const item = localStorage.getItem(key); return item ? JSON.parse(item) : defaultValue; } catch { return defaultValue; } });
const setValue = (value: T | ((prev: T) => T)) => { const newValue = typeof value === 'function' ? (value as (prev: T) => T)(state) : value; setState(newValue); localStorage.setItem(key, JSON.stringify(newValue)); };
return [state, setValue] as const;}Батчинг в React 18
Заголовок раздела «Батчинг в React 18»function Example() { const [a, setA] = useState(0); const [b, setB] = useState(0);
// React 18: один рендер на все обновления в одном обработчике function handleClick() { setA(1); // не рендер setB(2); // не рендер // рендер здесь, один раз }
// React 18: автоматический батчинг даже в setTimeout / Promise useEffect(() => { setTimeout(() => { setA(1); // не рендер (в React 17 был бы рендер) setB(2); // один рендер }, 1000); }, []);
// Отключить батчинг при необходимости function handleForceSync() { flushSync(() => setA(1)); // рендер немедленно flushSync(() => setB(2)); // ещё рендер }}Типичные ошибки
Заголовок раздела «Типичные ошибки»1. Прямая мутация состояния
Заголовок раздела «1. Прямая мутация состояния»// ❌ Мутация — React не увидит изменениеconst [user, setUser] = useState({ name: 'Иван' });function badUpdate() { user.name = 'Пётр'; // мутация объекта setUser(user); // один и тот же объект — нет рендера!}
// ✅ Новый объектfunction goodUpdate() { setUser({ ...user, name: 'Пётр' });}
// ❌ Мутация массиваconst [items, setItems] = useState([1, 2, 3]);function badPush() { items.push(4); // мутация setItems(items); // нет рендера}
// ✅ Новый массивfunction goodPush() { setItems([...items, 4]); // или setItems(prev => [...prev, 4]);}2. Использование устаревшего замыкания (stale closure)
Заголовок раздела «2. Использование устаревшего замыкания (stale closure)»// ❌ Проблема: count "заморожен" в замыканииfunction BadTimer() { const [count, setCount] = useState(0);
useEffect(() => { const interval = setInterval(() => { setCount(count + 1); // всегда 0 + 1 = 1! }, 1000); return () => clearInterval(interval); }, []); // пустой массив зависимостей — count никогда не обновляется
return <div>{count}</div>;}
// ✅ Функциональное обновление — всегда актуальный prevfunction GoodTimer() { const [count, setCount] = useState(0);
useEffect(() => { const interval = setInterval(() => { setCount(prev => prev + 1); // всегда актуальный }, 1000); return () => clearInterval(interval); }, []);
return <div>{count}</div>;}3. Избыточное состояние
Заголовок раздела «3. Избыточное состояние»// ❌ Производные данные в состоянииfunction BadComponent({ items }: { items: string[] }) { const [filteredItems, setFilteredItems] = useState(items); const [filter, setFilter] = useState('');
useEffect(() => { setFilteredItems(items.filter(i => i.includes(filter))); }, [items, filter]);}
// ✅ Вычисляй во время рендераfunction GoodComponent({ items }: { items: string[] }) { const [filter, setFilter] = useState(''); const filteredItems = items.filter(i => i.includes(filter)); // useMemo если дорого}Состояние форм
Заголовок раздела «Состояние форм»interface FormData { email: string; password: string;}
function LoginForm() { const [form, setForm] = useState<FormData>({ email: '', password: '' }); const [errors, setErrors] = useState<Partial<FormData>>({});
function handleChange(e: React.ChangeEvent<HTMLInputElement>) { const { name, value } = e.target; setForm(prev => ({ ...prev, [name]: value })); // Сброс ошибки при изменении поля setErrors(prev => ({ ...prev, [name]: undefined })); }
function validate(): boolean { const newErrors: Partial<FormData> = {}; if (!form.email.includes('@')) newErrors.email = 'Некорректный email'; if (form.password.length < 6) newErrors.password = 'Минимум 6 символов'; setErrors(newErrors); return Object.keys(newErrors).length === 0; }
function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (validate()) { // отправка } }
return ( <form onSubmit={handleSubmit}> <input name="email" value={form.email} onChange={handleChange} /> {errors.email && <span>{errors.email}</span>} <input name="password" type="password" value={form.password} onChange={handleChange} /> {errors.password && <span>{errors.password}</span>} <button type="submit">Войти</button> </form> );}