Шаг 2. Производное состояние (useMemo)
Добавим переключатель фильтра (все / активные / выполненные), строку поиска и счётчики. Ключевая идея шага: отфильтрованный список и счётчики не хранятся в состоянии — они вычисляются из задач во время рендера.
Постановка задачи
Заголовок раздела «Постановка задачи»Расширь TaskApp из шага 1:
- добавь состояние
filter: Filter('all' | 'active' | 'done') и кнопки переключения; - добавь состояние
search: stringи поле поиска; - вычисли видимый список задач: сначала применяется фильтр, потом поиск по
title(без учёта регистра); - покажи счётчики: всего задач, активных, выполненных;
- список и счётчики должны быть производными от
tasks+filter+search, а не отдельным состоянием.
Тип Filter берётся из src/types/task.ts.
Какие API и зачем
Заголовок раздела «Какие API и зачем»useState<Filter>иuseState<string>— фильтр и поисковый запрос это состояние UI: их меняет пользователь.- Вычисление во время рендера —
visibleTasksи счётчики выводятся из существующего состояния. Хранить их в отдельномuseState+ синхронизировать через эффект — антипаттерн (лишний рендер, рассинхрон). Просто посчитай в теле компонента. useMemo— нужен, только если вычисление дорогое (большой список, тяжёлая фильтрация) или результат передаётся в мемоизированный дочерний компонент. Мемоизация кэширует результат и пересчитывает его лишь при изменении зависимостей. Для маленького спискаuseMemoизбыточен — но мы покажем его как привычку для роста списка.
Показать эталонную реализацию
import { useMemo, useState } from 'react';import type { Task, Filter } from '../types/task';
interface Props { tasks: Task[]; onToggle: (id: string) => void; onRemove: (id: string) => void;}
export function TaskBoard({ tasks, onToggle, onRemove }: Props) { const [filter, setFilter] = useState<Filter>('all'); const [search, setSearch] = useState('');
// Видимый список — производное состояние. useMemo оправдан: фильтр + поиск // могут быть дорогими на большом списке, пересчитываем только при изменении deps. const visibleTasks = useMemo(() => { const byFilter = tasks.filter((t) => { if (filter === 'active') return !t.done; if (filter === 'done') return t.done; return true; // 'all' });
const query = search.trim().toLowerCase(); if (!query) return byFilter; return byFilter.filter((t) => t.title.toLowerCase().includes(query)); }, [tasks, filter, search]);
// Счётчики — тоже производные. Здесь вычисление дешёвое, useMemo не нужен. const total = tasks.length; const activeCount = tasks.filter((t) => !t.done).length; const doneCount = total - activeCount;
return ( <div> <input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Поиск..." />
<div> <button onClick={() => setFilter('all')} disabled={filter === 'all'}> Все ({total}) </button> <button onClick={() => setFilter('active')} disabled={filter === 'active'}> Активные ({activeCount}) </button> <button onClick={() => setFilter('done')} disabled={filter === 'done'}> Выполнено ({doneCount}) </button> </div>
<ul> {visibleTasks.map((task) => ( <li key={task.id}> <input type="checkbox" checked={task.done} onChange={() => onToggle(task.id)} /> <span style={{ textDecoration: task.done ? 'line-through' : 'none' }}> {task.title} </span> <button onClick={() => onRemove(task.id)}>×</button> </li> ))} </ul>
{visibleTasks.length === 0 && <p>Ничего не найдено</p>} </div> );}Ловушки
Заголовок раздела «Ловушки»Дальше — Шаг 3: загрузка данных и useEffect: подключим taskApi, состояния loading/error и разберём гонки запросов.