Мини-проект: список дел
Собираем классическое приложение todo и закрепляем иммутабельные операции с массивом в состоянии.
Todo-список — лучший первый проект на React: он объединяет состояние-массив, события, списки с ключами и иммутабельные обновления.
Состояние и добавление
Храним массив задач и текст нового пункта. Каждой задаче даём уникальный id (например, через Date.now()) и флаг done:
import { useState } from "react";
function TodoApp() {
const [todos, setTodos] = useState([]);
const [text, setText] = useState("");
function addTodo() {
if (text.trim() === "") return;
const newTodo = { id: Date.now(), text, done: false };
setTodos([...todos, newTodo]); // иммутабельно: новый массив
setText(""); // очищаем поле
}
// ... продолжение ниже
}
Удаление и переключение
Удаляем через filter, переключаем done через map — обе операции возвращают новый массив:
function removeTodo(id) {
setTodos(todos.filter((t) => t.id !== id));
}
function toggleTodo(id) {
setTodos(todos.map((t) =>
t.id === id ? { ...t, done: !t.done } : t
));
}
Обратите внимание на { ...t, done: !t.done }: создаём копию задачи с перевёрнутым флагом, а остальные задачи возвращаем как есть. Никаких мутаций.
Проверим логику на чистом JS
Все три операции — это чистые преобразования массива. Убедимся, что они работают:
let todos = [];
todos = [...todos, { id: 1, text: "Купить хлеб", done: false }];
todos = [...todos, { id: 2, text: "Учить React", done: false }];
// отметить задачу 1 выполненной
todos = todos.map(t => t.id === 1 ? { ...t, done: true } : t);
// удалить задачу 2
todos = todos.filter(t => t.id !== 2);
console.log(todos);
const left = todos.filter(t => !t.done).length;
console.log("Осталось активных:", left);
Вывод:
[ { id: 1, text: 'Купить хлеб', done: true } ]
Осталось активных: 0
Разметка
Поле ввода, кнопка и список с ключами. Зачёркнутые задачи помечаем классом:
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button onClick={addTodo}>Добавить</button>
<ul>
{todos.map((t) => (
<li key={t.id}>
<span
onClick={() => toggleTodo(t.id)}
className={t.done ? "done" : ""}
>
{t.text}
</span>
<button onClick={() => removeTodo(t.id)}>✕</button>
</li>
))}
</ul>
</div>
);
Какие концепции здесь работают
| Приём | Где в проекте |
| состояние-массив | todos |
| управляемое поле | text + onChange |
| иммутабельность | ..., map, filter |
| списки и key | todos.map с key={t.id} |
| события с аргументом | onClick={() => removeTodo(t.id)} |
Итог
- Todo-список объединяет все базовые приёмы: состояние-массив, управляемое поле, события, списки с ключами.
- Добавление — spread
[...todos, new], удаление —filter, переключение —mapс копией объекта. - Все операции иммутабельны: создаём новый массив, не мутируя старый.
Проверьте себя
1. Как иммутабельно переключить флаг done у одной задачи в массиве?
Atodos.find(t => t.id === id).done = !done
Btodos.map(t => t.id === id ? { ...t, done: !t.done } : t)
Ctodos.push({ done: true })
Dtodos[id].done = true
2. Каким методом удобно удалить задачу по id иммутабельно?
Asplice
Bpush
Cfilter
Dpop
3. Зачем каждой задаче нужен уникальный id?
AДля красоты
BЧтобы ускорить рендер вдвое
Cid не нужен
DЧтобы использовать его как key в списке и адресно удалять/менять задачу