JS: Архитектура фронтенда
2026-02-26 17:06 Diff

Современные фронтенд-приложения включают множество элементов, которые должны корректно реагировать на изменения данных: спиннеры крутятся, кнопки отключаются, данные отправляются. Управление этим процессом требует продуманного подхода. В идеале, любые изменения в интерфейсе являются следствием изменения данных, то есть состояния приложения. Представьте себе форму регистрации, у которой кнопка отправки (submit) заблокирована во время выполнения запроса на сервер (с точки зрения UX это обязательно для любых форм). В таком случае состояние может приобрести следующий вид:

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

В реальных приложениях все еще сложнее. Во время отправки данных блокируется не только кнопка отправки, но и поле для ввода. Более того, отправка данных в одном месте, может повлиять и на остальные блоки на странице, которые могут пропадать, блокироваться или видоизменяться. Не говоря уже о том, что причин блокировки кнопки может бы��ь несколько. Она может быть заблокирована просто потому, что в форму введены некорректные данные.

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

Но такой подход очень быстро усложняет разработку. Когда состояние описывается через множество независимых флагов, может возникнуть логическая путаница. Например, если форма валидна (valid: true), но submitDisabled: true, пользователь не сможет отправить ее, хотя логически это должно быть возможно. При увеличении числа таких флагов усложняется понимание взаимосвязей и проверок.

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

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

В примере выше большая часть флагов связана с процессом обработки данных формы. Предположим, что после отправки формы, данные уходят на сервер, затем от него приходит ответ и дальше результат отображается пользователю. Результат может быть как успешным, так и не успешным. Мы должны продумывать все исходы. Весь процесс условно можно разбить на несколько промежуточных состояний:

Предложенный набор не является универсальным. Процессы могут быть устроены сложнее, а значит потребуется другой набор состояний.

  • filling – заполнение формы. В этом состоянии все активно и доступно для редактирования.
  • processing (или sending) – отправка формы. Это то самое состояние, когда пользователь ждет, а приложение пытается предотвратить нежелательные действия, например, клики или изменения данных формы.
  • processed (или finished) – состояние, обозначающее, что все завершилось. В нем форма уже не отображается.
  • failed – состояние, обозначающее завершение с ошибкой. Например, произошел сбой в сети во время загрузки или загруженные данные оказались неверными.

Использование одного свойства state вместо нескольких флагов упрощает логику. Например, состояние processing автоматически определяет, что форма заблокирована, спиннер активен, а кнопка submit отключена — и для этого не нужно держать три отдельных флага. Такой подход снижает вероятность багов и делает код более читаемым.

Перепишем наше состояние убрав оттуда все флаги и введя одно свойство отвечающее за состояние работы с формой:

В состоянии мы выделили процесс регистрации, который может принимать одно из возможных состояний. Даже такое, на первый взгляд, небольшое изменение резко упрощает систему. Теперь нам не нужно отслеживать каждый участвующий в этом процессе элемент. Главное, чтобы все возможные состояния описывали все возможные варианты поведения. Тогда все проверки в выводе сведутся к проверке общего состояния:

Кроме таких состояний, есть различные данные, сопровождающие наш процесс. Например, processed может завершиться с ошибками. В таком случае можно ввести дополнительно массив (или объект, в зависимости от структуры) с ошибками, который будет заполняться при их наличии:

Причем этот же массив с ошибками удобно использовать для валидации формы до отправки на сервер. То есть будучи в состоянии filling.

А что, если мы захотим блокировать возможность отправки формы до того момента, пока не пройдет валидация на фронтенде? Есть два подхода: либо мы проверяем, что errors пуст, либо, что лучше, мы вводим явное состояние валидности формы. И тогда состояние нашего приложения становится таким:

В некоторых ситуациях возможно объединение, когда процесс валидации соединен с процессом обработки самой регистрации. Тогда вместо отдельного состояния validationState, появится дополнительное состояние invalid внутри state. Это не совсем корректно с точки зрения моделирования (потому что у нас действительно два разных процесса), но иногда такой способ позволяет написать чуть более простой код (до тех пор пока различий не станет много).

Глобально, такой подход в разработке называется программированием с явным выделенным состоянием. Он сводится к тому, что в рамках приложения находятся базовые процессы, от которых зависит все остальное. Причем не важно, какие инструменты используются для разработки: чистый DOM, jQuery или любой мощный современный фреймворк. Он применим везде и везде нужен.

Собирая все вместе

Пример ниже демонстрирует этот подход на простой форме регистрации.

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

Разбор кода

  • Явное состояние state.registrationProcess.state

    • filling – ввод данных в поле.
    • processing – отправка формы, блокировка кнопки и поля.
    • success – успешная регистрация, кнопка отключается.
    • failed – ошибка валидации, отображается сообщение.
  • Функция updateUI()

    • Управляет блокировкой кнопки и полем ввода.
    • Показывает сообщения "Отправка...", "Ошибка..." или "Успешно отправлено!".
    • Блокирует кнопку, если валидация не пройдена.
  • Валидация email при вводе

    • Если email пуст, кнопка Submit остается заблокированной.
    • Ошибка отображается сразу, без необходимости отправки.
  • Отправка формы (submit обработчик)

    • При клике кнопка блокируется, состояние "processing".
    • Через setTimeout() эмулируется серверный ответ.
    • Если email валиден → success, иначе → failed.

Это невероятно мощная парадигма программирования, которая описана в книге "Автоматное Программирование" в наших рекомендациях.