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

Шаг 1. Состояние списка (useState)

Первый рабочий экран: список задач, форма добавления, чекбокс «выполнено» и кнопка удаления. Пока без сервера — задачи живут в состоянии компонента.

Реализуй компонент TaskApp, который:

  • хранит массив задач в состоянии (стартовое значение — пустой массив или пара задач);
  • показывает форму с полем ввода и кнопкой «Добавить»; по сабмиту создаёт новую задачу (crypto.randomUUID() для id, done: false, createdAt: Date.now()) и добавляет её в список;
  • по клику на чекбокс переключает done у конкретной задачи;
  • по кнопке «удалить» убирает задачу из списка;
  • не добавляет пустые задачи (тримит ввод).

Тип Task берётся из src/types/task.ts (см. Окружение).

  • 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, где добавим фильтры, счётчики и поиск.