Зарегистрируйтесь, чтобы продолжить обучение

Состояние отображения (UI State) JS: Архитектура фронтенда

Изменение состояния фронтенд-приложения не всегда означает изменение данных, с которыми работает приложение. У данных может быть состояние, которое влияет только на внешний вид. Такое состояние называется UI-состоянием, то есть состоянием интерфейса пользователя. Его особенность в том, что оно существует только на клиенте во время взаимодействия с интерфейсом.

Например, в интернет-магазине у карточки товара может быть состояние "в фокусе" при наведении курсора. Это влияет только на отображение (например, карточка увеличивается или меняет цвет), но не меняет данные о товаре. Другой пример — индикатор загрузки. Когда пользователь отправляет форму, интерфейс может отображать спиннер или затемнять кнопку отправки. Это состояние актуально только во время запроса и не сохраняется в базе данных. Еще один пример — раскрытие или сворачивание списка комментариев. Сам факт того, что пользователь нажал кнопку "Показать больше", изменяет только локальное состояние отображения, но не сами комментарии и их содержание в базе данных.

Представьте себе обычный аккордеон. Это способ отображения данных, с помощью которого можно скрыть или раскрыть какой-то из элементов списка. Для работы подобного аккордеона нужно состояние, описывающее отображение каждого элемента: свернут/раскрыт.

Bootstrap Accordion

Где должно храниться это состояние? Например, его можно поместить внутрь самих данных:

// Список компаний. За отображение в аккордеоне отвечает флаг visibility
const state = {
  companies: [
    // Данные, которые пришли с сервера
    {
      id: 1,
      name: "Hexlet",
      description: "online courses",
      visibility: "hidden", // UI состояние, которое добавили на клиенте
    },
    {
      id: 2,
      name: "Google",
      description: "search engine",
      visibility: "shown", // UI
    },
    {
      id: 3,
      name: "Facebook",
      description: "social network",
      visibility: "hidden", // UI
    },
  ],
};

Где-то дальше, в слое отображения, происходит вывод этих данных с учетом флага. Технически задача решена, но у данного способа хранения есть существенные недостатки.

Начнем с главного. Данные на фронтенде не появляются из ниоткуда. Данные приложения хранятся на сервере, приходят с сервера и уходят на сервер. А сервер ничего про внешний вид не знает и знать не должен, это не касается данных. UI-состояние временное и изменяется только на клиенте. И тут возникает серьезная проблема. Если UI-состояние хранится внутри данных, то придется постоянно выполнять две вещи:

  • Вводить дополнительную обработку для всех приходящих данных с сервера, добавляя туда UI-состояние.
  • Вводить дополнительную обработку для всех данных, уходящих на сервер, удаляя из них все UI-состояние.

А подобных элементов отображения, как правило, значительно больше, чем один. Сюда можно отнести видимость модальных окон, сортировку, скрытие/раскрытие во всех возможных проявлениях, различные режимы (редактирование), подтверждения и многое другое. Все это придется не просто хранить внутри данных, но и постоянно помнить про необходимость дополнительной обработки.

Но это еще не все. Далеко не всегда весь набор данных обрабатывается одинаково. Возможно, что один набор данных выводится на странице в разных местах либо только частично. Это значит, что UI-состояние у разных элементов может быть разное, либо у каких-то элементов его может не быть вообще. Как поступать в таком случае? Игнорировать различия и добавлять всем одинаковый набор данных или усложнять логику и делать заполнение выборочным?

Из-за перечисленных проблем UI-состояние хранят отдельно от самих данных. Причем для каждой ситуации это будет свой набор данных. Например, для аккордеона состояние может выглядеть так:

const state = {
  companies: [...],
  uiState: {
    accordion: [
      { companyId: 1, visibility: 'hidden' },
      { companyId: 2, visibility: 'shown' },
      { companyId: 3, visibility: 'hidden' },
    ],
  },
};

В какой момент это состояние появляется внутри 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 companies = [
        { id: 1, name: "Hexlet", description: "Онлайн-курсы" },
        { id: 2, name: "Google", description: "Поисковая система" },
        { id: 3, name: "Facebook", description: "Социальная сеть" },
      ];

      const ui = {
        accordion: { [companies[0].id]: true },
      };

      const state = { companies, ui };

      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.addEventListener("click", () => toggleAccordion(company.id));

          const body = document.createElement("div");
          body.textContent = company.description;
          if (!state.ui.accordion[company.id]) {
            body.style.display = "none";
          }

          card.appendChild(header);
          card.appendChild(body);
          container.appendChild(card);
        });
      }

      function toggleAccordion(companyId) {
        state.ui.accordion[companyId] = !state.ui.accordion[companyId];
        renderAccordion();
      }

      renderAccordion();
    </script>
  </body>
</html>

Попрактиковаться

Плюсы и минусы разделения

Отделяя UI-состояние от основных данных, мы получаем значительные преимущества:

  • Упрощение взаимодействия с сервером. Данные, отправляемые и получаемые от сервера, остаются чистыми и понятными. UI-состояние обрабатывается исключительно на клиенте, без дополнительной очистки или обогащения.

  • Повышение гибкости. Поскольку интерфейс может иметь несколько представлений одних и тех же данных, отдельное хранение UI-состояния позволяет легко настраивать каждое представление независимо друг от друга.

  • Удобство отладки и тестирования. Разделение данных и UI-состояния облегчает поиск и устранение ошибок, так как можно отдельно проверять корректность данных и отдельно проверять состояние интерфейса.

Однако при таком подходе появляется новая задача: нужно поддерживать синхронизацию данных и UI-состояния. Изменение данных, например, удаление компании из списка, должно приводить к соответствующим изменениям в UI-состоянии, иначе возможны ошибки и некорректное отображение.

function removeCompany(companyId) {
  // Удаление компании из данных
  state.companies = state.companies.filter(
    (company) => company.id !== companyId,
  );
  // Синхронизация UI-состояния
  state.ui.accordion = state.ui.accordion.filter(
    (item) => item.companyId !== companyId,
  );
}

// Пример использования
removeCompany(2);

Для автоматизации таких задач применяют:

  • Общую функцию-обработчик, которая управляет и данными, и UI-состоянием.
  • Реактивные инструменты управления состоянием (Redux, Zustand, MobX), автоматически синхронизирующие изменения.

Итог

Разделение UI-состояния и данных позволяет снизить сложность и повысить управляемость приложения, упрощает работу с сервером и облегчает тестирование. Однако требует продуманной синхронизации между состоянием интерфейса и основными данными. Применение подходящих инструментов и практик помогает эффективно решать эти задачи.


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
Верстка на HTML5 и CSS3, Программирование на JavaScript в браузере, разработка клиентских приложений используя React
10 месяцев
с нуля
Старт 13 марта
профессия
Программирование на JavaScript в браузере и на сервере (Node.js), разработка бекендов на Fastify и фронтенда на React
16 месяцев
с нуля
Старт 13 марта

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»