Шаг 1. Состояние списка (useState)
Первый рабочий экран: список задач, форма добавления, чекбокс «выполнено» и кнопка удаления. Пока без сервера — задачи живут в состоянии компонента.
Постановка задачи
Заголовок раздела «Постановка задачи»Реализуй компонент TaskApp, который:
- хранит массив задач в состоянии (стартовое значение — пустой массив или пара задач);
- показывает форму с полем ввода и кнопкой «Добавить»; по сабмиту создаёт новую задачу (
crypto.randomUUID()дляid,done: false,createdAt: Date.now()) и добавляет её в список; - по клику на чекбокс переключает
doneу конкретной задачи; - по кнопке «удалить» убирает задачу из списка;
- не добавляет пустые задачи (тримит ввод).
Тип Task берётся из src/types/task.ts (см. Окружение).
Какие API и зачем
Заголовок раздела «Какие API и зачем»useState<Task[]>— массив задач это локальное состояние UI, которое меняется по действиям пользователя.useState— базовый инструмент для такого.- Функциональная форма
setTasks(prev => ...)— все три операции (добавить/переключить/удалить) вычисляют новое состояние из предыдущего. Функциональная форма гарантирует актуальныйprevи защищает от проблем при нескольких обновлениях подряд. - Иммутабельность (spread,
map,filter) — React сравнивает состояние по ссылке. Нельзя мутировать массив (push) или объект задачи напрямую — нужно создавать новый массив/объект, иначе ре-рендер не произойдёт. useState<string>для поля ввода — контролируемый input: значение поля хранится в состоянии,onChangeего обновляет.
Показать эталонную реализацию
import { useState } from 'react';import type { Task } from '../types/task';
export function TaskApp() { const [tasks, setTasks] = useState<Task[]>([]); const [draft, setDraft] = useState('');
function addTask(title: string) { const trimmed = title.trim(); if (!trimmed) return; // не добавляем пустые
const task: Task = { id: crypto.randomUUID(), title: trimmed, done: false, createdAt: Date.now(), }; // новый массив, а не push — иначе React не увидит изменение setTasks((prev) => [...prev, task]); setDraft(''); }
function toggleTask(id: string) { setTasks((prev) => prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t)), ); }
function removeTask(id: string) { setTasks((prev) => prev.filter((t) => t.id !== id)); }
function handleSubmit(e: React.FormEvent) { e.preventDefault(); addTask(draft); }
return ( <section> <h1>Task Tracker</h1>
<form onSubmit={handleSubmit}> <input value={draft} onChange={(e) => setDraft(e.target.value)} placeholder="Новая задача..." /> <button type="submit">Добавить</button> </form>
<ul> {tasks.map((task) => ( <li key={task.id}> <input type="checkbox" checked={task.done} onChange={() => toggleTask(task.id)} /> <span style={{ textDecoration: task.done ? 'line-through' : 'none' }}> {task.title} </span> <button onClick={() => removeTask(task.id)}>×</button> </li> ))} </ul> </section> );}Ловушки
Заголовок раздела «Ловушки»Дальше — Шаг 2: производное состояние и useMemo, где добавим фильтры, счётчики и поиск.