Мини-проект: список дел
Собираем классическое приложение 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
Bfilter
Cpush
Dpop
3. Зачем каждой задаче нужен уникальный id?
AДля красоты
BЧтобы использовать его как key в списке и адресно удалять/менять задачу
CЧтобы ускорить рендер вдвое
Did не нужен