$ sudo teach IT

Модуль 11: React + TypeScript

Урок 11.3: Состояние (useState) с типами

Продвинутый уровень 55 минут

1. Основы useState с типизацией

Хук useState — это один из основных хуков React, который позволяет компонентам иметь собственное состояние. При работе с TypeScript правильная типизация состояния критически важна для обеспечения безопасного кода.

В этом уроке мы подробно разберём все аспекты типизации состояния в React-компонентах, от простых случаев до сложных сценариев с вложенными объектами и массивами.

Базовый синтаксис useState

Хук useState принимает начальное значение и возвращает кортеж из двух элементов: текущее значение состояния и функцию для его обновления.

// Простые типы состояния
const [count, setCount] = useState(0); // count имеет тип number
const [name, setName] = useState(''); // name имеет тип string
const [isActive, setIsActive] = useState(false); // isActive имеет тип boolean
const [items, setItems] = useState(null); // items имеет тип null
// Явное указание типа (когда начальное значение не может определить тип)
const [user, setUser] = useState<User | null>(null); const [data, setData] = useState<string[]>([]); const [selectedId, setSelectedId] = useState<number | undefined>(undefined);

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

Когда нужно явно указывать тип?

  1. Когда начальное значение — пустой массив [] (TypeScript не может определить тип элементов)
  2. Когда начальное значение — null или undefined, а позже будет другое значение
  3. Когда нужен union-тип (например, string | null)
  4. Когда используете сложныеgenerics типы

2. Типизация сложных состояний

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

Состояние-объект

// Определяем интерфейс для формы
interface LoginFormState { email: string; password: string; rememberMe: boolean; isLoading: boolean; error: string | null; } const LoginForm = () => { // Начальное значение определяет тип состояния
const [formState, setFormState] = useState<LoginFormState>({ email: '', password: '', rememberMe: false, isLoading: false, error: null, }); // Правильное обновление объектного состояния
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => { setFormState(prev => ({ ...prev, email: e.target.value, })); }; // Обновление вложенного свойства
const handleRememberMe = (e: React.ChangeEvent<HTMLInputElement>) => { setFormState(prev => ({ ...prev, rememberMe: e.target.checked, })); }; // Асинхронное обновление с обработкой ошибок
const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setFormState(prev => ({ ...prev, isLoading: true, error: null })); try { await login(formState.email, formState.password); } catch (err) { setFormState(prev => ({ ...prev, isLoading: false, error: err instanceof Error ? err.message : 'Произошла ошибка', })); } }; };

Состояние-массив

// Типизация массива объектов
interface Todo { id: number; text: string; completed: boolean; } const [todos, setTodos] = useState<Todo[]>([]); // Добавление нового элемента в массив
const addTodo = (text: string) => { const newTodo: Todo = { id: Date.now(), text, completed: false, }; setTodos(prev => [...prev, newTodo]); }; // Обновление элемента в массиве
const toggleTodo = (id: number) => { setTodos(prev => prev.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo ) ); }; // Удаление элемента из массива
const removeTodo = (id: number) => { setTodos(prev => prev.filter(todo => todo.id !== id)); }; // Типизация состояния с union-типом
type Status = 'idle' | 'loading' | 'success' | 'error'; const [status, setStatus] = useState<Status>('idle'); // Обработка состояний с narowing
const renderContent = () => { switch (status) { case 'idle': return <div>Начальное состояние</div>; case 'loading': return <div>Загрузка...</div>; case 'success': return <div>Успешно!</div>; case 'error': return <div>Ошибка!</div>; } };

3. Типизация callback-функций в useState

Функции обновления состояния (set-функции) также нуждаются в правильной типизации. Давайте разберём основные паттерны.

Типизация setter-функций

// Простая setter-функция
const [name, setName] = useState(''); // setName принимает string | ((prev: string) => string)
// Setter с предыдущим значением (functional update)
const increment = () => { setCount(prev => prev + 1); // prev имеет тип number
}; // Setter с условным обновлением
const updateName = (newName: string) => { setName(prev => { if (newName.length > 50) { return prev; // Возвращаем предыдущее значение
} return newName; }); }; // Типизация сложной setter-функции
interface UserState { name: string; email: string; settings: { theme: 'light' | 'dark'; notifications: boolean; }; } const [user, setUser] = useState<UserState>({ name: '', email: '', settings: { theme: 'light', notifications: true, }, }); // Обновление вложенного объекта
const toggleTheme = () => { setUser(prev => ({ ...prev, settings: { ...prev.settings, theme: prev.settings.theme === 'light' ? 'dark' : 'light', }, })); }; // Безопасное обновление вложенного массива
interface CartItem { id: number; name: string; quantity: number; } const [cart, setCart] = useState<CartItem[]>([]); const updateQuantity = (id: number, quantity: number) => { setCart(prev => prev.map(item => item.id === id ? { ...item, quantity: Math.max(0, quantity) } : item ) ); };

4. Типизация с useReducer

Хук useReducer является альтернативой useState для управления сложным состоянием. Он особенно полезен, когда состояние содержит множество подстатей или когда логика обновления состояния сложная.

Базовый синтаксис useReducer

// Определяем типы для состояния и действий
interface CounterState { count: number; step: number; } type CounterAction = | { type: 'increment' } | { type: 'decrement' } | { type: 'setStep'; payload: number } | { type: 'reset' }; // Начальное состояние
const initialState: CounterState = { count: 0, step: 1, }; // Редьюсер с полной типизацией
function counterReducer( state: CounterState, action: CounterAction ): CounterState { 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; } } // Использование в компоненте
const Counter = () => { const [state, dispatch] = useReducer(counterReducer, initialState); return ( <div> <p>Счётчик: {state.count}</p> <p>Шаг: {state.step}</p> <button onClick={() => dispatch({ type: 'increment' })}>+</button> <button onClick={() => dispatch({ type: 'decrement' })}>-</button> <button onClick={() => dispatch({ type: 'setStep', payload: 5 })}>Установить шаг 5</button> <button onClick={() => dispatch({ type: 'reset' })}>Сброс</button> </div> ); };

Продвинутый пример: Типизация API-состояния

// Типизация состояния загрузки данных
interface ApiState<T> { data: T | null; loading: boolean; error: string | null; } type ApiAction<T> = | { type: 'FETCH_START' } | { type: 'FETCH_SUCCESS'; payload: T } | { type: 'FETCH_ERROR'; payload: string }; function apiReducer<T>( state: ApiState<T>, action: ApiAction<T> ): ApiState<T> { switch (action.type) { case 'FETCH_START': return { ...state, loading: true, error: null }; case 'FETCH_SUCCESS': return { data: action.payload, loading: false, error: null }; case 'FETCH_ERROR': return { ...state, loading: false, error: action.payload }; default: return state; } } // Кастомный хук для работы с API
function useApi<T>(fetchFn: () => Promise<T>) { const [state, dispatch] = useReducer(apiReducer<T>, { data: null, loading: true, error: null, }); useEffect(() => { const loadData = async () => { dispatch({ type: 'FETCH_START' }); try { const data = await fetchFn(); dispatch({ type: 'FETCH_SUCCESS', payload: data }); } catch (err) { dispatch({ type: 'FETCH_ERROR', payload: err instanceof Error ? err.message : 'Неизвестная ошибка', }); } }; loadData(); }, []); return state; }

5. Типизация useState сgenerics

Generics в useState позволяют создавать более гибкие и переиспользуемые компоненты. Давайте разберём продвинутые техники использованияgenerics.

Кастомный хук сgenerics

// Кастомный хук для формы сgenerics
function useForm<T extends Record<string, any>>( initialValues: T, validate?: (values: T) => Partial<Record<keyof T, string>> ) { const [values, setValues] = useState<T>(initialValues); const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({}); const [isSubmitting, setIsSubmitting] = useState(false); const handleChange = (field: keyof T, value: T[keyof T]) => { setValues(prev => ({ ...prev, [field]: value, })); }; const handleSubmit = async ( onSubmit: (values: T) => Promise<void> ) => { if (validate) { const validationErrors = validate(values); setErrors(validationErrors); if (Object.keys(validationErrors).length > 0) { return; } } setIsSubmitting(true); try { await onSubmit(values); } finally { setIsSubmitting(false); } }; const reset = () => { setValues(initialValues); setErrors({}); }; return { values, errors, isSubmitting, handleChange, handleSubmit, reset, }; } // Использование кастомного хука
interface LoginFormValues { email: string; password: string; } const form = useForm<LoginFormValues>({ email: '', password: '', }); // form.values имеет тип LoginFormValues
// form.handleChange принимает keyof LoginFormValues и значение

6. Типизация localStorage и sessionStorage

Работа с localStorage и sessionStorage — распространённая задача в React-приложениях. Давайте создадим типизированные обёртки для этих API.

// Типизированная обёртка для localStorage
function useLocalStorage<T>( key: string, initialValue: T ): [T, (value: T | (val: T) => T) => void] { const [storedValue, setStoredValue] = useState<T>(() => { try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch (error) { console.error(error); return initialValue; } }); const setValue = (value: T | (val: T) => T) => { try { const valueToStore = value instanceof Function ? value(storedValue) : value; setStoredValue(valueToStore); window.localStorage.setItem(key, JSON.stringify(valueToStore)); } catch (error) { console.error(error); } }; return [storedValue, setValue]; } // Использование:
const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light'); const [user, setUser] = useLocalStorage<User | null>('user', null);

7. Типизация состояния формы

Управление формами — одна из самых частых задач в React. Давайте создадим полноценный пример типизированной формы с валидацией.

// Полный пример типизированной формы регистрации
interface RegistrationFormState { username: string; email: string; password: string; confirmPassword: string; acceptTerms: boolean; } interface FormErrors { username?: string; email?: string; password?: string; confirmPassword?: string; acceptTerms?: string; } const RegistrationForm = () => { const [formData, setFormData] = useState<RegistrationFormState>({ username: '', email: '', password: '', confirmPassword: '', acceptTerms: false, }); const [errors, setErrors] = useState<FormErrors>({}); const validate = (): FormErrors => { const newErrors: FormErrors = {}; if (!formData.username.trim()) { newErrors.username = 'Имя пользователя обязательно'; } else if (formData.username.length < 3) { newErrors.username = 'Имя должно быть не менее 3 символов'; } if (!formData.email) { newErrors.email = 'Email обязателен'; } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { newErrors.email = 'Некорректный email'; } if (!formData.password) { newErrors.password = 'Пароль обязателен'; } else if (formData.password.length < 8) { newErrors.password = 'Пароль должен быть не менее 8 символов'; } if (formData.password !== formData.confirmPassword) { newErrors.confirmPassword = 'Пароли не совпадают'; } if (!formData.acceptTerms) { newErrors.acceptTerms = 'Необходимо принять условия'; } return newErrors; }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const validationErrors = validate(); setErrors(validationErrors); if (Object.keys(validationErrors).length === 0) { console.log('Форма валидна:', formData); } }; return ( <form onSubmit={handleSubmit}> {/* Поля формы с типизированными обработчиками */}
</form> ); };

8. Практическое задание

Выполните следующие задания для закрепления материала:

Задание 1: Counter с useReducer

  1. Создайте интерфейс CounterState с полями: count (number), step (number), history (number[])
  2. Определите тип CounterAction для действий: increment, decrement, setStep, reset, undo
  3. Реализуйте редьюсер с полной типизацией
  4. Создайте компонент Counter с отображением истории

Задание 2: Кастомный хук useForm

  1. Создайтеgenerics хук useForm, который принимает начальные значения формы
  2. Добавьте функцию validate, которая принимает функцию валидации
  3. Реализуйте автоматическую валидацию при изменении полей
  4. Используйтеgenerics для получения типизированных значений формы

Задание 3: useLocalStorage

  1. Создайтеgenerics хук useLocalStorage с полной типизацией
  2. Добавьте обработку ошибок при чтении/записи
  3. Реализуйте синхронизацию между вкладками через event storage
  4. Создайте пример использования с темой (light/dark) и пользователем

9. Итоги урока

В этом уроке мы подробно изучили:

  • Базовую типизацию useState для примитивных типов
  • Типизацию сложных состояний (объекты, массивы, union-типы)
  • Типизацию callback-функций в useState
  • Использование useReducer для сложного состояния
  • generics для создания переиспользуемых хуков
  • Типизацию localStorage и sessionStorage
  • Полный пример типизированной формы с валидацией

Советы и лучшие практики

  1. Всегда указывайте типы для useState, когда начальное значение не определяет тип
  2. Используйте functional update (prev => ...) для обновления состояния на основе предыдущего
  3. Применяйте useReducer для сложной логики состояния
  4. Создавайтеgenerics хуки для переиспользования логики
  5. Избегайте мутации состояния — всегда создавайте новые объекты/массивы

В следующем уроке мы разберём типизацию хуков и создание кастомных хуков с TypeScript.

Отличная работа! Вы освоили типизацию состояния в React!

Тест по useState с типами

1 вопрос

Тест по обновлению состояния

1 вопрос

Тест по useReducer

1 вопрос

Типизированный useState (функциональная версия)

Premium