Todo-лист становится понятным, когда массив tasks является единственным источником истины. Интерфейс не хранит отдельную копию состояния: каждое действие меняет массив, сохраняет JSON и заново рисует подходящие задачи.
Этот проект тренирует формы, массив объектов, события, фильтрацию и локальное хранилище. Главная ловушка не в кнопке удаления, а в синхронизации. Если менять HTML отдельно от данных, после перезагрузки появятся расхождения.
Каждое действие создаёт новое состояние массива.
После изменения записываем JSON в localStorage.
Фильтр выбирает данные, render строит список через textContent.
Какие данные нужны одной задаче
Объект задачи содержит id, text и done. id связывает кнопку в интерфейсе с объектом, text хранит ввод, done отвечает за статус. Текущий filter можно оставить в памяти: после перезагрузки приложение откроется со всеми задачами.
| Поле | Тип | Пример |
|---|---|---|
| id | string | crypto.randomUUID() |
| text | string | Проверить мобильное меню |
| done | boolean | false |
| filter | string | all, active, done |
После этого шага у каждой части есть одна роль. Интерфейс собирает действие, логика меняет состояние, а вывод показывает признак успеха. Это проще отлаживать, чем один большой обработчик.

Данные проходят одинаковый путь. HTML никогда не становится источником истины
Пишем добавление, переключение и удаление
Ниже ядро приложения. JSON.parse обёрнут в try/catch, потому что хранилище может содержать старые или повреждённые данные. Текст задачи назначается через textContent, а не вставляется как HTML.
const KEY='codik.tasks';
let filter='all';
let tasks=load();
function load(){
try{const value=JSON.parse(localStorage.getItem(KEY) ?? '[]');return Array.isArray(value)?value:[]}
catch{return []}
}
function save(){localStorage.setItem(KEY,JSON.stringify(tasks))}
function change(next){tasks=next;save();render()}
function add(text){
const clean=text.trim(); if(!clean) return;
change([...tasks,{id:crypto.randomUUID(),text:clean,done:false}]);
}
function toggle(id){change(tasks.map(task=>task.id===id?{...task,done:!task.done}:task))}
function remove(id){change(tasks.filter(task=>task.id!==id))}
function render(){
const list=document.querySelector('#tasks'); list.replaceChildren();
const visible=tasks.filter(task=>filter==='all'||(filter==='done')===task.done);
for(const task of visible){
const item=document.createElement('li');
const label=document.createElement('button'); label.textContent=task.text;
label.className=task.done?'done':''; label.onclick=()=>toggle(task.id);
const del=document.createElement('button'); del.textContent='Удалить';del.onclick=()=>remove(task.id);
item.append(label,del);list.append(item);
}
}
render();Массив tasks является единственным состоянием задач. Пустая строка не добавляется. Текст выводится через textContent. Повреждённый localStorage безопасно даёт пустой список.

Пользовательский ввод остаётся данными. Сравнение показывает, какой вариант легче поддерживать и проверять.
Как безопасно хранить JSON в браузере
localStorage хранит только строки и работает синхронно. Для десятков учебных задач это нормально. Большие данные и сложный поиск лучше переносить в IndexedDB или на сервер. Важно не путать сохранение с аккаунтом: задачи останутся только в этом браузере и профиле.
| Действие | Операция массива | После неё |
|---|---|---|
| Добавить | spread + новый объект | save, render |
| Переключить | map | save, render |
| Удалить | filter | save, render |
| Показать активные | filter для отображения | только render |
Сохраняйте в одном месте то, что может измениться. Если одно и то же значение скопировано в три функции, после первой правки они начнут расходиться. Один объект состояния и одна функция отрисовки снимают эту проблему.
Практика: проверяем фильтры и перезапуск
Добавьте три задачи, выполните среднюю, включите каждый фильтр и перезагрузите страницу. Удалите одну задачу из режима «Выполненные». Затем вручную испортите значение codik.tasks в DevTools. Приложение должно запуститься с пустым списком. Проверьте две задачи с одинаковым текстом: переключение и удаление должны затрагивать только выбранный id. Затем вставьте строку с угловыми скобками и убедитесь, что она показана как текст, а не созданный HTML-элемент. Откройте приложение во второй вкладке и измените список в первой. Учебная версия не обязана синхронизироваться мгновенно, но после обновления должна прочитать последнее сохранённое состояние. Это хороший мостик к событию storage и серверной синхронизации.
- Добавьте пустую строку и строку с пробелами.
- Создайте три нормальные задачи.
- Переключите одну два раза.
- Удалите задачу в отфильтрованном списке.
- Перезагрузите страницу.
- Испортите JSON в localStorage и снова откройте приложение.
Пустые задачи не появляются. Статус и удаление работают во всех фильтрах. После F5 список восстанавливается. Текст с угловыми скобками остаётся текстом.

Четыре сценария состояния. Пройдите все четыре сценария перед публикацией проекта.
Доводим Todo-лист до готового приложения
После базовой версии добавьте редактирование и очистку выполненных. Все новые действия проводите через change, чтобы не забыть save и render. Если появится сервер, интерфейсный контракт останется похожим: состояние меняется после успешного запроса.
После фильтра и F5 данные расходятся.
Строка может превратиться в разметку.
Повреждённое значение ломает запуск.
Текст не подходит для точного удаления.
Что получится в итоге

Готовый Todo-лист показывает активные и выполненные задачи, считает остаток и сохраняет всё между перезагрузками.
Что является источником истины?
Массив объектов tasks, а HTML лишь отображает его.
Почему нужен id?
Он позволяет точно найти задачу независимо от текста и фильтра.
Чем безопаснее выводить текст?
Свойством textContent.
Как понять, что проект готов?
Он повторяет целевой сценарий несколько раз подряд, понятно реагирует на ошибочный ввод и не теряет состояние при обычных действиях.
Можно ли копировать код целиком?
Можно как стартовую точку, но сначала замените значения из примера, затем запустите проект по шагам. Если вы не можете объяснить одну строку, её лучше разобрать до следующего шага.
Массивы, формы и DOM разбираются в курсе JavaScript. Если элемент не найден, поможет разбор ошибки null.
Для синхронизации с сервером изучите Fetch API, а внешний вид дополните тёмной темой. В Кодике удобно сохранять рабочие этапы: добавление, фильтры, хранилище.