Учебный проект TODO-листа для React

Шаг 1. Установка React-проекта

npm create vite@latest todo-app -- --template react-ts
cd todo-app
npm install

Шаг 2. Структура проекта

├── eslint.config.js
├── index.html
├── package.json
├── public
│   └── vite.svg
├── README.md
├── src
│   ├── App.css
│   ├── App.jsx
│   ├── assets
│   ├── index.css
│   └── main.jsx
└── vite.config.js

  • App.jsx — главный компонент.
  • main.jsx — точка входа (React подключается к HTML).

Шаг 3. Пишем простую версию ToDo

В App.jsx начнём с базового:

import { useState } from "react";

function App() {
  const [tasks, setTasks] = useState([]); // массив задач
  const [newTask, setNewTask] = useState(""); // текст новой задачи

  const addTask = () => {
    if (newTask.trim() === "") return;
    setTasks([...tasks, newTask]);
    setNewTask("");
  };

  return (
    <div style="{{" padding:="" "20px",="" fontfamily:="" "sans-serif"="" }}="">
      <h1>📋 Мой список задач</h1>

      <input value="{newTask}" onchange="{(e)" ==""> setNewTask(e.target.value)}
        placeholder="Введите задачу..."
      />
      <button onclick="{addTask}">Добавить</button>

      <ul>
        {tasks.map((task, index) =&gt; (
          <li key="{index}">{task}</li>
        ))}
      </ul>
    </div>
  );
}

export default App;

В src/App.css:

.app-container {
  max-width: 500px;
  margin: 40px auto;
  padding: 20px;
  font-family: sans-serif;
  background: #f5f5f5;
  border-radius: 8px;
}

.input-container {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

input {
  flex: 1;
  padding: 8px;
  font-size: 16px;
}

button {
  padding: 8px 16px;
  font-size: 16px;
  cursor: pointer;
}

.task-list {
  list-style: none;
  padding: 0;
}

.task-list li {
  padding: 6px 0;
  border-bottom: 1px solid #ddd;
}

Шаг 4. Запускаем проект

 npm run dev 

Откроется страница с полем ввода, кнопкой и списком задач.

Шаг 5. Добавляем удаление задачи

В App.jsx меняем логику так:

import { useState } from "react";
import "./App.css";

function App() {
  const [tasks, setTasks] = useState([]);
  const [newTask, setNewTask] = useState("");

  const addTask = () => {
    if (newTask.trim() === "") return;
    setTasks([...tasks, { text: newTask, completed: false }]);
    setNewTask("");
  };

  const deleteTask = (index) => {
    setTasks(tasks.filter((_, i) => i !== index));
  };

  const toggleTask = (index) => {
    setTasks(
      tasks.map((task, i) =>
        i === index ? { ...task, completed: !task.completed } : task
      )
    );
  };

  return (
    

📋 Мой список задач

setNewTask(e.target.value)} />
    {tasks.map((task, index) => (
  • toggleTask(index)}>{task.text}
  • ))}
); } export default App;
Оператор … («spread» — распыление) берёт все элементы массива и вставляет их в новом месте.
const arr1 = [1, 2, 3];
const arr2 = […arr1, 4];
console.log(arr2); // [1, 2, 3, 4]

В App.css добавим:

.task-list li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  cursor: pointer;
}

.task-list li span {
  flex: 1;
}
  • Теперь задача хранится как объект { text, completed }.
  • Клик по тексту задачи переключает её состояние (выполнена / не выполнена).
  • Кнопка ❌ удаляет задачу.
  • React при каждом изменении отрисовывает только изменившиеся элементы, а не всю страницу — это ключевая идея.

Шаг 6. Хранение списка задач в localStorage

План действий

  1. При загрузке страницы — читать задачи из localStorage и класть их в tasks.
  2. При каждом изменении задач — сохранять их в localStorage.

Для этого используем хук useEffect, который в React позволяет выполнять побочные действия при рендере.

Код с localStorage

Открой App.jsx и обнови его так:

import { useState, useEffect } from "react";
import "./App.css";

function App() {
  const [tasks, setTasks] = useState(() => {
    const saved = localStorage.getItem("tasks");
    return saved ? JSON.parse(saved) : [];
  });

  const [newTask, setNewTask] = useState("");

  // Сохраняем задачи при каждом изменении
  useEffect(() => {
    localStorage.setItem("tasks", JSON.stringify(tasks));
  }, [tasks]);

  const addTask = () => {
    if (newTask.trim() === "") return;
    setTasks([...tasks, { text: newTask, completed: false }]);
    setNewTask("");
  };

  const deleteTask = (index) => {
    setTasks(tasks.filter((_, i) => i !== index));
  };

  const toggleTask = (index) => {
    setTasks(
      tasks.map((task, i) =>
        i === index ? { ...task, completed: !task.completed } : task
      )
    );
  };

  return (
    

📋 Мой список задач

setNewTask(e.target.value)} />
    {tasks.map((task, index) => (
  • toggleTask(index)}>{task.text}
  • ))}
); } export default App;