Мини-проект: список дел

Собираем классическое приложение 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
списки и keytodos.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 не нужен
Поддержать проект