Изменение состояния фронтенд-приложения не всегда означает изменение данных, с которыми работает приложение. У данных может быть состояние, которое влияет только на внешний вид. Такое состояние называется UI-состоянием, то есть состоянием интерфейса пользователя. Его особенность в том, что оно существует только на клиенте во время взаимодействия с интерфейсом.
Например, в интернет-магазине у карточки товара может быть состояние "в фокусе" при наведении курсора. Это влияет только на отображение (например, карточка увеличивается или меняет цвет), но не меняет данные о товаре. Другой пример — индикатор загрузки. Когда пользователь отправляет форму, интерфейс может отображать спиннер или затемнять кнопку отправки. Это состояние актуально только во время запроса и не сохраняется в базе данных. Еще один пример — раскрытие или сворачивание списка комментариев. Сам факт того, что пользователь нажал кнопку "Показать больше", изменяет только локальное состояние отображения, но не сами комментарии и их содержание в базе данных.
Представьте себе обычный аккордеон. Это способ отображения данных, с помощью которого можно скрыть или раскрыть какой-то из элементов списка. Для работы подобного аккордеона нужно состояние, описывающее отображение каждого элемента: свернут/раскрыт.
Где должно храниться это состояние? Например, его можно поместить внутрь самих данных:
// Список компаний. За отображение в аккордеоне отвечает флаг visibility
const state = {
companies: [
// Данные, которые пришли с сервера
{
id: 1,
name: 'Hexlet',
description: 'Онлайн-курсы',
visibility: 'hidden', // UI-состояние
},
{
id: 2,
name: 'Yandex',
description: 'Поисковая система',
visibility: 'shown', // UI-состояние
},
{
id: 3,
name: 'VK',
description: 'Социальная сеть',
visibility: 'hidden', // UI-состояние
},
],
}
Где-то дальше, в слое отображения, происходит вывод этих данных с учетом флага. Технически задача решена, но у данного способа хранения есть существенные недостатки.
Начнем с главного. Данные на фронтенде не появляются из ниоткуда. Данные приложения хранятся на сервере, приходят с сервера и уходят на сервер. А сервер ничего про внешний вид не знает и знать не должен, это не касается данных. UI-состояние временное и изменяется только на клиенте. И тут возникает серьезная проблема. Если UI-состояние хранится внутри данных, то придется постоянно выполнять две вещи:
- Вводить дополнительную обработку для всех приходящих данных с сервера, добавляя туда UI-состояние.
- Вводить дополнительную обработку для всех данных, уходящих на сервер, удаляя из них все UI-состояние.
А подобных элементов отображения, как правило, значительно больше, чем один. Сюда можно отнести видимость модальных окон, сортировку, скрытие/раскрытие во всех возможных проявлениях, различные режимы (редактирование), подтверждения и многое другое. Все это придется не просто хранить внутри данных, но и постоянно помнить про необходимость дополнительной обработки.
Но это еще не все. Далеко не всегда весь набор данных обрабатывается одинаково. Возможно, что один набор данных выводится на странице в разных местах либо только частично. Это значит, что UI-состояние у разных элементов может быть разное, либо у каких-то элементов его может не быть вообще. Как поступать в таком случае? Игнорировать различия и добавлять всем одинаковый набор данных или усложнять логику и делать заполнение выборочным?
Из-за перечисленных проблем UI-состояние хранят отдельно от самих данных. Причем для каждой ситуации это будет свой набор данных. Например, для аккордеона состояние может выглядеть так:
const state = {
companies: [
// ...
],
uiState: {
accordion: {
1: false, // свернут
2: true, // раскрыт
3: false, // свернут
},
},
}
В какой момент это состояние появляется внутри state
? UI-состояние может формироваться как в процессе работы приложения, так и на этапе инициализации при его запуске. Например:
- При запуске приложения: состояние видимости элементов аккордеона задаётся по умолчанию (
visibility: 'hidden'
). - Во время работы: состояние модального окна изменяется на visible, когда пользователь кликает по кнопке открытия этого окна.
Пример: Реализация аккордиона
Соберем все вмести и посмотрим на код аккордиона, который демонстрирует принцип работы с UI-состоянием. В этом примере используется подход инициализации состояния по запросу (кроме первого элемента, который должен быть показан сразу).
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>UI-состояние в аккордеоне</title>
</head>
<body>
<div id="accordion"></div>
<script>
const state = {
companies: [
{ id: 1, name: 'Hexlet', description: 'Онлайн-курсы' },
{ id: 2, name: 'Google', description: 'Поисковая система' },
{ id: 3, name: 'VK', description: 'Социальная сеть' },
],
uiState: {
accordion: { 1: true }, // по умолчанию раскрыт только первый элемент
},
}
function renderAccordion() {
const container = document.getElementById('accordion')
container.innerHTML = ''
state.companies.forEach(company => {
const card = document.createElement('div')
const header = document.createElement('div')
header.textContent = company.name
header.style.fontWeight = 'bold'
header.style.cursor = 'pointer'
header.addEventListener('click', () => toggleAccordion(company.id))
const body = document.createElement('div')
body.textContent = company.description
body.style.display = state.uiState.accordion[company.id] ? 'block' : 'none'
card.appendChild(header)
card.appendChild(body)
container.appendChild(card)
})
}
function toggleAccordion(companyId) {
state.uiState.accordion[companyId] = !state.uiState.accordion[companyId]
renderAccordion()
}
renderAccordion()
</script>
</body>
</html>
Попрактиковаться
Плюсы и минусы разделения
Отделяя UI-состояние от основных данных, мы получаем значительные преимущества:
Упрощение взаимодействия с сервером. Данные, отправляемые и получаемые от сервера, остаются чистыми и понятными. UI-состояние обрабатывается исключительно на клиенте, без дополнительной очистки или обогащения.
Повышение гибкости. Поскольку интерфейс может иметь несколько представлений одних и тех же данных, отдельное хранение UI-состояния позволяет легко настраивать каждое представление независимо друг от друга.
Удобство отладки и тестирования. Разделение данных и UI-состояния облегчает поиск и устранение ошибок, так как можно отдельно проверять корректность данных и отдельно проверять состояние интерфейса.
Однако при таком подходе появляется новая задача: нужно поддерживать синхронизацию данных и UI-состояния. Изменение данных, например, удаление компании из списка, должно приводить к соответствующим изменениям в UI-состоянии, иначе возможны ошибки и некорректное отображение.
function removeCompany(companyId) {
// Удаляем компанию
state.companies = state.companies.filter(c => c.id !== companyId)
// Синхронизируем UI-состояние
delete state.uiState.accordion[companyId]
}
// Пример
removeCompany(2)
Полный пример
Для автоматизации таких задач применяют:
- Общую функцию-обработчик, которая управляет и данными, и UI-состоянием.
- Реактивные инструменты управления состоянием (Redux, Zustand, MobX), автоматически синхронизирующие изменения.
Итог
Разделение UI-состояния и данных позволяет снизить сложность и повысить управляемость приложения, упрощает работу с сервером и облегчает тестирование. Однако требует продуманной синхронизации между состоянием интерфейса и основными данными. Применение подходящих инструментов и практик помогает эффективно решать эти задачи.
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.