JS: Архитектура фронтенда
Теория: Программирование с явно выделенным состоянием
Современные фронтенд-приложения включают множество элементов, которые должны корректно реагировать на изменения данных: спиннеры крутятся, кнопки отключаются, данные отправляются. Управление этим процессом требует продуманного подхода. В идеале, любые изменения в интерфейсе являются следствием изменения данных, то есть состояния приложения. Представьте себе форму регистрации, у которой кнопка отправки (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.statefilling– ввод данных в поле.processing– отправка формы, блокировка кнопки и поля.success– успешная регистрация, кнопка отключается.failed– ошибка валидации, отображается сообщение.
-
Функция updateUI()
- Управляет блокировкой кнопки и полем ввода.
- Показывает сообщения "Отправка...", "Ошибка..." или "Успешно отправлено!".
- Блокирует кнопку, если валидация не пройдена.
-
Валидация email при вводе
- Если
emailпуст, кнопкаSubmitостается заблокированной. - Ошибка отображается сразу, без необходимости отправки.
- Если
-
Отправка формы (
submitобработчик)- При клике кнопка блокируется, состояние "processing".
- Через
setTimeout()эмулируется серверный ответ. - Если email валиден →
success, иначе →failed.
Это невероятно мощная парадигма программирования, которая описана в книге "Автоматное Программирование" в наших рекомендациях.




