Перейти к содержимому

useState

import { useState } from 'react';
function Counter() {
// [текущее значение, функция обновления]
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Счётчик: {count}
</button>
);
}
// Тип выводится автоматически
const [count, setCount] = useState(0); // number
const [name, setName] = useState(''); // string
const [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());
// Пример: чтение из localStorage
function 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;
}
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)); // ещё рендер
}
}
// ❌ Мутация — 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>;
}
// ✅ Функциональное обновление — всегда актуальный prev
function GoodTimer() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(prev => prev + 1); // всегда актуальный
}, 1000);
return () => clearInterval(interval);
}, []);
return <div>{count}</div>;
}
// ❌ Производные данные в состоянии
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>
);
}