Шаг 5. Сложное состояние (useReducer + useContext)
Состояние разрослось: задачи, фильтр, поиск, плюс несколько операций. Разрозненные useState и проброс обработчиков через несколько уровней (prop-drilling) становятся неудобными. Соберём всё в useReducer и раздадим через useContext.
Постановка задачи
Заголовок раздела «Постановка задачи»- Опиши состояние приложения одним объектом:
{ tasks, filter, search }; - опиши действия размеченным объединением (discriminated union):
add,toggle,remove,setFilter,setSearch; - напиши чистый reducer, обрабатывающий каждое действие иммутабельно;
- создай Context, который отдаёт
stateиdispatch, и провайдерTaskProvider; - напиши типобезопасный хук
useTaskContext, бросающий ошибку вне провайдера; - любой вложенный компонент (форма, список, фильтры) берёт нужное через хук, без проброса пропсов.
Тип Task и Filter — из src/types/task.ts.
Какие API и зачем
Заголовок раздела «Какие API и зачем»useReducer(reducer, initialState)— когда состояние сложное (несколько связанных полей) и обновления нетривиальны, reducer собирает всю логику в одном чистом месте. Возвращает[state, dispatch].- Discriminated union для actions — каждое действие имеет поле
type; TypeScript по нему сужает тип и проверяет полнотуswitch. Невозможно отправить действие с неверным payload. - Чистый reducer — функция
(state, action) => newStateбез побочных эффектов. Её легко тестировать отдельно от React. createContext+useContext— раздаётstateиdispatchвглубь дерева без prop-drilling. Дефолтnull+ проверка в хуке ловят использование вне провайдера.
Показать эталонную реализацию
import { createContext, useContext, useReducer, type Dispatch } from 'react';import type { Task, Filter } from '../types/task';
// 1. Состояниеinterface State { tasks: Task[]; filter: Filter; search: string;}
// 2. Действия — discriminated uniontype Action = | { type: 'add'; title: string } | { type: 'toggle'; id: string } | { type: 'remove'; id: string } | { type: 'setFilter'; filter: Filter } | { type: 'setSearch'; search: string };
const initialState: State = { tasks: [], filter: 'all', search: '' };
// 3. Чистый reducerfunction reducer(state: State, action: Action): State { switch (action.type) { case 'add': { const title = action.title.trim(); if (!title) return state; const task: Task = { id: crypto.randomUUID(), title, done: false, createdAt: Date.now(), }; return { ...state, tasks: [...state.tasks, task] }; } case 'toggle': return { ...state, tasks: state.tasks.map((t) => t.id === action.id ? { ...t, done: !t.done } : t, ), }; case 'remove': return { ...state, tasks: state.tasks.filter((t) => t.id !== action.id) }; case 'setFilter': return { ...state, filter: action.filter }; case 'setSearch': return { ...state, search: action.search }; default: return state; // exhaustiveness: TS проверит, что все type обработаны }}
// 4. Contextinterface TaskContextValue { state: State; dispatch: Dispatch<Action>;}
const TaskContext = createContext<TaskContextValue | null>(null);
export function TaskProvider({ children }: { children: React.ReactNode }) { const [state, dispatch] = useReducer(reducer, initialState); return ( <TaskContext.Provider value={{ state, dispatch }}> {children} </TaskContext.Provider> );}
// 5. Типобезопасный хукexport function useTaskContext(): TaskContextValue { const ctx = useContext(TaskContext); if (!ctx) throw new Error('useTaskContext must be used within TaskProvider'); return ctx;}
// Любой вложенный компонент берёт нужное без prop-drilling:export function FilterTabs() { const { state, dispatch } = useTaskContext(); const filters: Filter[] = ['all', 'active', 'done']; return ( <div> {filters.map((f) => ( <button key={f} disabled={state.filter === f} onClick={() => dispatch({ type: 'setFilter', filter: f })} > {f} </button> ))} </div> );}Ловушки
Заголовок раздела «Ловушки»Дальше — Шаг 6: кастомные хуки, где вынесем логику в переиспользуемые хуки.