Состояние и перерисовка

проект≈ 7 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Разметка на месте, кнопки нажимаются — осталось решить, кто в приложении главный. Дальше главным будет обычный массив tasks: он решает, что видно на экране, а функция render просто переводит его в интерфейс.

Разделы темы:·

Экран ничего не помнит. Он показывает то, что ему нарисовали, и если спросить у него, сколько задач осталось невыполненными, придётся пересчитывать строки разметки. Поэтому правду держат отдельно — в обычном массиве объектов.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Как выглядит одна задача, решено в плане — в разделе про формат задачи: объект с полями id, title и done. Здесь важно другое: всё, что видит пользователь, — производное от массива таких объектов: и список, и счётчик оставшихся дел, и надпись о том, что дел больше нет. Такой массив называют состоянием приложения.

render — функция без сюрпризов: получает массив, возвращает то, что нужно показать. Она ничего не запоминает между вызовами, поэтому один и тот же массив всегда даёт один и тот же экран.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Разметки у этого примера нет — вставлять некуда, — поэтому render просто возвращает строку, а мы её печатаем. В настоящем проекте отличается ровно одна строчка: вместо console.log будет container.innerHTML = render(tasks). Смысл тот же — экран собирается из состояния целиком, а не подправляется по кусочкам.

Добавление задачи — операция над массивом, а не над экраном. Спред ... копирует то, что было, новая задача приезжает в конец, и сразу за этим зовётся render.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Удаление — это filter: он оставляет всё, что не совпало по id. Обратите внимание, что мы ни разу не искали строку на экране — мы искали объект в массиве.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

tasks.length + 1 живёт ровно до первого удаления: убрали вторую из трёх — и следующая новая задача получит уже занятый номер. Надёжнее счётчик, который только растёт.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

В браузере есть ещё Date.now() и crypto.randomUUID() — оба честно годятся для id, но возвращают разное при каждом запуске, поэтому в примерах учебника мы их не печатаем.

Переключение — самая тонкая из трёх операций: меняется одна задача, а остальные обязаны остаться прежними. map проходит по всем и подменяет только совпавшую по id.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Выражение { ...t, done: !t.done } — копия задачи с перевёрнутым флагом. Старый объект мы не трогаем: так проще откатывать изменения, сравнивать версии состояния и искать ошибки, а библиотеки вроде React именно на таком сравнении и держатся.

Мутация тоже сработает, экран обновится. Но прежнее состояние исчезнет — сравнить старый массив с новым будет уже не с чем, а на этом сравнении строятся и отмена действия, и экономная перерисовка.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Сложим правило, по которому живёт весь проект: событие меняет состояние, изменение состояния вызывает render, render рисует экран. Обратной стрелки нет — ни один обработчик не имеет права дописать что-то в интерфейс мимо состояния. Удобно завести единственную дверь setState, через которую проходят все изменения.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Что делает пользовательЧто происходит с массивомЧто рисует 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 у нужной задачи. Исходный массив должен остаться нетронутым.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

render склеивает строки через map и join, а toggle подменяет только совпавшую задачу — остальные объекты возвращаются как есть.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Проверка в последней строке важна: если бы вы написали t.done = !t.done, здесь выводилось бы true, и старого состояния уже не существовало бы.

Кнопка «Убрать выполненные»

Напишите clearDone(list) — он возвращает новый массив только из невыполненных задач. Проверьте, что длина исходного массива не изменилась.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Условие фильтра — просто отрицание флага: оставляем задачи, у которых done ложный.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

В приложении обработчик кнопки состоял бы из двух строк: tasks = clearDone(tasks); и вызов render(tasks) — больше ему делать нечего.