Состояние и перерисовка
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Разметка на месте, кнопки нажимаются — осталось решить, кто в приложении главный. Дальше главным будет обычный массив tasks: он решает, что видно на экране, а функция render просто переводит его в интерфейс.
Экран ничего не помнит. Он показывает то, что ему нарисовали, и если спросить у него, сколько задач осталось невыполненными, придётся пересчитывать строки разметки. Поэтому правду держат отдельно — в обычном массиве объектов.
Как выглядит одна задача, решено в плане — в разделе про формат задачи: объект с полями id, title и done. Здесь важно другое: всё, что видит пользователь, — производное от массива таких объектов: и список, и счётчик оставшихся дел, и надпись о том, что дел больше нет. Такой массив называют состоянием приложения.
render — функция без сюрпризов: получает массив, возвращает то, что нужно показать. Она ничего не запоминает между вызовами, поэтому один и тот же массив всегда даёт один и тот же экран.
Разметки у этого примера нет — вставлять некуда, — поэтому render просто возвращает строку, а мы её печатаем. В настоящем проекте отличается ровно одна строчка: вместо console.log будет container.innerHTML = render(tasks). Смысл тот же — экран собирается из состояния целиком, а не подправляется по кусочкам.
Добавление задачи — операция над массивом, а не над экраном. Спред ... копирует то, что было, новая задача приезжает в конец, и сразу за этим зовётся render.
Удаление — это filter: он оставляет всё, что не совпало по id. Обратите внимание, что мы ни разу не искали строку на экране — мы искали объект в массиве.
tasks.length + 1 живёт ровно до первого удаления: убрали вторую из трёх — и следующая новая задача получит уже занятый номер. Надёжнее счётчик, который только растёт.
В браузере есть ещё Date.now() и crypto.randomUUID() — оба честно годятся для id, но возвращают разное при каждом запуске, поэтому в примерах учебника мы их не печатаем.
Переключение — самая тонкая из трёх операций: меняется одна задача, а остальные обязаны остаться прежними. map проходит по всем и подменяет только совпавшую по id.
Выражение { ...t, done: !t.done } — копия задачи с перевёрнутым флагом. Старый объект мы не трогаем: так проще откатывать изменения, сравнивать версии состояния и искать ошибки, а библиотеки вроде React именно на таком сравнении и держатся.
Мутация тоже сработает, экран обновится. Но прежнее состояние исчезнет — сравнить старый массив с новым будет уже не с чем, а на этом сравнении строятся и отмена действия, и экономная перерисовка.
Сложим правило, по которому живёт весь проект: событие меняет состояние, изменение состояния вызывает render, render рисует экран. Обратной стрелки нет — ни один обработчик не имеет права дописать что-то в интерфейс мимо состояния. Удобно завести единственную дверь setState, через которую проходят все изменения.
| Что делает пользователь | Что происходит с массивом | Что рисует render |
|---|---|---|
| Ввёл текст и нажал «Добавить» | новый массив со спредом | список на строку длиннее |
| Нажал на крестик | filter по id | строка исчезла |
| Кликнул по галочке | map с копией одной задачи | галочка сменила вид |
Тянет поправить экран напрямую — значит, чего-то не хватает в состоянии. Добавьте поле, а не строчку в разметку.
Такой поток называют однонаправленным. Он скучный, и в этом вся ценность: чтобы понять, почему на экране три задачи, достаточно посмотреть на tasks, а не восстанавливать историю кликов.
- Состояние приложения — массив задач; экран лишь отображает его и сам ничего не помнит.
- render получает состояние и собирает интерфейс целиком, ничего не храня между вызовами.
- Добавление — спред, удаление — filter, переключение — map с копией нужного объекта.
- Каждое изменение состояния заканчивается вызовом render: к экрану ведёт ровно один путь.
- Однонаправленный поток: событие меняет данные, данные рисуют экран, обратной стрелки нет.
1/3Пользователь нажал крестик на задаче. Что делает обработчик в приложении с однонаправленным потоком?
2/3Что вернёт вызов tasks.filter((t) => t.id !== 2)?
3/3Зачем render перерисовывает весь список, а не только изменившуюся строку?
Соберите render и toggle
Напишите render(list), который возвращает строку вида [ ] Хлеб | [x] Сыр, и toggle(list, id), который возвращает новый массив с перевёрнутым done у нужной задачи. Исходный массив должен остаться нетронутым.
render склеивает строки через map и join, а toggle подменяет только совпавшую задачу — остальные объекты возвращаются как есть.
Проверка в последней строке важна: если бы вы написали t.done = !t.done, здесь выводилось бы true, и старого состояния уже не существовало бы.
Кнопка «Убрать выполненные»
Напишите clearDone(list) — он возвращает новый массив только из невыполненных задач. Проверьте, что длина исходного массива не изменилась.
Условие фильтра — просто отрицание флага: оставляем задачи, у которых done ложный.
В приложении обработчик кнопки состоял бы из двух строк: tasks = clearDone(tasks); и вызов render(tasks) — больше ему делать нечего.