- Пример: Управление списком задач
- Пример: Отсутствие источника правды
- Выделение состояния
- Пример: Управление списком задач (с выделенным состоянием)
- Итого
Манипулирование DOM — задача простая только в самых примитивных ситуациях. Как только понадобится реализовать полноценное, пусть и небольшое приложение, сложность кода начинает расти в экспоненциальной прогрессии. Почему так происходит? Проблема в том, что при увеличении данных для работы, количества событий и элементов в DOM, код становится запутанным, а отладка и внесение изменений — настоящей головной болью.
Ниже мы на примерах рассмотрим про то, почему это происходит и что надо сделать, чтобы этого избежать.
Пример: Управление списком задач
Допустим, у нас есть простой To-Do список. Мы хотим добавить возможность:
- Добавлять новые задачи.
- Удалять задачи.
- Отмечать задачи как выполненные.
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<title>To-Do List</title>
</head>
<body>
<input type="text" id="taskInput" />
<button id="addTask">Добавить</button>
<ul id="taskList"></ul>
<script>
const taskInput = document.getElementById("taskInput");
const addTaskButton = document.getElementById("addTask");
const taskList = document.getElementById("taskList");
addTaskButton.addEventListener("click", () => {
const taskText = taskInput.value.trim();
if (taskText === "") return;
const li = document.createElement("li");
li.textContent = taskText;
const deleteButton = document.createElement("button");
deleteButton.textContent = "Удалить";
deleteButton.addEventListener("click", () => {
li.remove();
});
const doneButton = document.createElement("button");
doneButton.textContent = "Готово";
doneButton.addEventListener("click", () => {
li.style.textDecoration = "line-through";
});
li.appendChild(deleteButton);
li.appendChild(doneButton);
taskList.appendChild(li);
taskInput.value = "";
});
</script>
</body>
</html>
Почему этот код быстро становится неуправляемым?
Логика работы и логика отображения перемешаны, из-за чего трудно понять, какие изменения связаны с состоянием приложения, а какие просто обновляют интерфейс. Обработчики напрямую изменяют DOM, усложняя отслеживание последовательности событий. По мере роста кода становится все труднее разобраться, как именно он работает. Дополнительную сложность создает отсутствие списка задач вне DOM — из-за этого невозможно напрямую работать с данными. Если нужно, например, отобразить количество невыполненных задач или сохранить состояние между перезагрузками, придется вручную извлекать информацию из DOM, что увеличивает вероятность ошибок и делает код еще сложнее.
Если развить этот пример включив туда редактирование, синхронизацию с бекендом и другие возможности, то мы упремся в то, что для выполнения любого изменения, нужно потратить невероятное количество времени и мозговых усилий для понимания происходящего, особенно там где, где события зависят друг от друга.
Пример: Отсутствие источника правды
Когда у нас из инструментов только DOM, то мы автоматически строим всю работу вокруг него, например, храним данные в data-*
атрибутах или прямо в значениях элементов. В примере ниже происходит увеличение счетчика. Каждое нажатие на кнопку увеличения должно откуда-то брать текущее значения счетчика, а так как оно хранится только в элементе result
, то он и используется.
const result = document.getElementById('result');
const inc = document.getElementById('increment');
inc.addEventListener('click', () => {
// Данные для работы извлекаются из DOM (result)
result.textContent = parseInt(result.textContent, 10) + 1;
});
};
app();
Попрактиковаться
Главная проблема такого подхода в отсутствии единого источника правды. Одни и те же данные, на одной странице, могут использоваться множество раз. Причем иногда по-разному. Представьте себе ситуацию, в которой нужно вывести количество отображаемых статей на странице. Для того чтобы их посчитать, придется писать такой код:
// Каждая статья обозначается классом .article
const articles = document.querySelectorAll(".article");
articles.length; // количество статей
А если у нас часть статей выводится в одном блоке, а часть в другом? Тогда придется обращаться к каждому из этих блоков, чтобы собрать все статьи. Чем дальше мы будем фантазировать, тем больше сложностей возникнет с тем, как работать с этими данными: добавлять, обновлять и использовать. Глядя на код, невозможно легко понять, откуда брать данные. Кроме того, они сильно завязаны на конкретную структуру верстки.
Выделение состояния
Примеры выше, это то как фронтенд приложения не пишутся. Подобный подход используется только для микроскопических вставок в проекты, где js нет или его крайне мало. Во фронтендовых приложениях, код выглядит совсем по другому. И первый шаг в эту сторону - научиться отделять состояние приложения от его представления на экране (UI).
Подойдем к правильной архитектуре со стороны бэкенда. В бэкенде приложения состоят минимум из двух частей — базы данных и собственно самого кода приложения. По сути, в типичных веб-проектах приложение занимается двумя процессами: либо обновляет данные в базе, либо извлекает эти данные и на основе них формирует HTML.
Необходимость базы данных на бекенде, обычно, не вызывает вопросов, но то же самое не очевидно во фронтенде, хотя правильное решение именно такое. И речь не идет про внедрение настоящей базы данных, такое бывает нужно не так уж часто. Достаточно хранить данные в отдельном объекте и разделить приложение на две части: ту что обновляет данные и ту, что на базе этих данных рисует UI.
При такой организации кода вырисовывается следующая схема работы:
- Возникает событие.
- Обработчик события меняет состояние.
- DOM обновляется на основе новых данных.
Ниже реализация этой идеи на примере простого счетчика. Состояние в данном случае — одно число. Кнопка с плюсом увеличивает его на единицу, кнопка с минусом соответственно уменьшает.
const app = () => {
let counterValue = 0;
const result = document.getElementById("result");
const incHandler = () => {
counterValue += 1;
result.textContent = counterValue;
};
const decHandler = () => {
counterValue -= 1;
result.textContent = counterValue;
};
const inc = document.getElementById("increment");
inc.addEventListener("click", incHandler);
const dec = document.getElementById("decrement");
dec.addEventListener("click", decHandler);
};
app();
Попрактиковаться
Здесь нет никаких обращений к DOM для извлечения текущего значения, оно хранится в переменной и доступно для всех обработчиков. Подобная организация упрощает не только хранение и работу с данными, но и отладку. В любой момент можно посмотреть внутрь состояния и сопоставить его с тем что выводится на экран. Фактически, внешний вид страницы становится отражением состояния приложения на экране.
Кода стало больше, но это только потому, что в этом примере практически отсутствует логика отображения, мы работаем только с одним DOM-элементом. В типовых примерах кода будет примерно одинаково, но сложность расширения и понимания у последнего примера гораздо ниже.
Пример: Управление списком задач (с выделенным состоянием)
Теперь перепишем наш пример с задачами внедрив туда отдельный объект состояния, в котором мы будем хранить задачи.
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<title>To-Do List</title>
</head>
<body>
<input type="text" id="taskInput" placeholder="Введите задачу" />
<button id="addTask">Добавить</button>
<ul id="taskList"></ul>
<script>
const state = {
tasks: [],
};
const taskInput = document.getElementById("taskInput");
const addTaskButton = document.getElementById("addTask");
const taskList = document.getElementById("taskList");
addTaskButton.addEventListener("click", () => {
const taskText = taskInput.value.trim();
if (taskText === "") return;
const task = { text: taskText };
state.tasks.push(task);
const li = document.createElement("li");
li.textContent = task.text;
// Кнопка "Удалить"
const deleteButton = document.createElement("button");
deleteButton.textContent = "Удалить";
deleteButton.addEventListener("click", () => {
const index = state.tasks.indexOf(task);
state.tasks.splice(index, 1);
li.remove();
});
const doneButton = document.createElement("button");
doneButton.textContent = "Готово";
doneButton.addEventListener("click", () => {
li.style.textDecoration = "line-through";
});
li.appendChild(deleteButton);
li.appendChild(doneButton);
taskList.appendChild(li);
taskInput.value = "";
});
</script>
</body>
</html>
Прямо сейчас может показаться, что изменение не очень сильно повлияло на происходящее в коде. Отчасти это правда, потому что выделение состояние это первая часть, вторая важная часть это отделение отрисовки UI. Вот тогда станет окончательно понятно по каким принципам и почему строятся фронтенд приложения на любом современном фреймворке. Об этом мы поговорим в следующих уроках.
Итого
Мы прошли путь от хаотичного кода, где всё хранится в DOM, до архитектурного подхода с состоянием. Теперь:
- UI не хранит данные, а только их отображает.
- Легко добавить новые функции, например, фильтрацию по невыполненным задачам.
Этот принцип — основа фронтенд-разработки. Он используется в React, Vue, Svelte и всех остальных фреймворках.
Дополнительные материалы

Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.