Чем сложнее фронтенд-приложение, тем больше различных элементов оно содержит. Каждый из этих элементов как-то реагирует на то, что происходит вокруг: спиннеры крутятся, кнопки выключаются, меню появляются и пропадают, данные отправляются.
В идеале, любые изменения в интерфейсе являются следствием изменения данных, то есть состояния приложения. Представьте себе форму регистрации, у которой кнопка отправки (submit) заблокирована во время выполнения запроса на сервер (с точки зрения UX это обязательно для любых форм). В таком случае состояние может приобрести следующий вид:
const state = {
registrationProcess: {
valid: true,
submitDisabled: true,
isLoading: true,
}
};
В реальных приложениях все еще сложнее. Во время отправки данных блокируется не только кнопка отправки, но и поле для ввода. Более того, отправка данных в одном месте, может повлиять и на остальные блоки на странице, которые могут пропадать, блокироваться или видоизменяться. Не говоря уже о том, что причин блокировки кнопки может быть несколько. Она может быть заблокирована просто потому, что в форму введены некорректные данные.
Если решать эту задачу в лоб, получится состояние с большим количеством флагов:
const state = {
registrationProcess: {
valid: true,
submitDisabled: true,
inputDisabled: true,
showSpinner: true,
blockAuthentication: true,
}
};
Каждый флаг отвечает за свой элемент на экране. С ростом количества флагов, начнет усложняться логика обновления состояния (нужно согласовывать их между собой и не забывать обновлять) и логика вывода (начнут появляться варианты зависимости внешнего вывода от разных флагов).
Проблема данного подхода в том, что он опирается не на причины происходящего, а на их следствия. Изменение активности кнопки, блокирование элементов, отображение спиннеров — все это следствия каких-то процессов. Умение выделить эти процессы и правильно описать в состоянии, один из краеугольных камней хорошей архитектуры.
В примере выше большая часть флагов связана с процессом обработки данных формы. Предположим, что после отправки формы, данные уходят на сервер, затем от него приходит ответ и дальше результат отображается пользователю. Результат может быть как успешным, так и не успешным. Мы должны продумывать все исходы. Весь процесс условно можно разбить на несколько промежуточных состояний:
Предложенный набор не является универсальным. Процессы могут быть устроены сложнее, а значит потребуется другой набор состояний. И имена состояний это причастия.
- filling – заполнение формы. В этом состоянии все активно и доступно для редактирования.
- processing (или sending) – отправка формы. Это то самое состояние, когда пользователь ждет, а приложение пытается предотвратить нежелательные действия, например, клики или изменения данных формы.
- processed (или finished) – состояние, обозначающее, что все завершилось. В нем форма уже не отображается.
- failed – состояние, обозначающее завершение с ошибкой. Например, произошел сбой в сети во время загрузки или загруженные данные оказались неверными.
С точки зрения теории автоматов (а мы имеем дело с автоматным программированием в данном случае), такие состояния называются управляющими. Они определяют то, где мы сейчас находимся. Перепишем наше состояние:
const state = {
registrationProcess: {
state: 'filling', // 'processing', 'processed', 'failed'
}
};
В состоянии мы выделили процесс регистрации, который может принимать одно из возможных состояний. Даже такое, на первый взгляд, небольшое изменение резко упрощает систему. Теперь нам не нужно отслеживать каждый участвующий в этом процессе элемент. Главное, чтобы все возможные состояния описывали все возможные варианты поведения. Тогда все проверки в выводе сведутся к проверке общего состояния:
// Этих "ифов" может быть сколько угодно,
// главное, что они завязаны на общее состояние, а не проверку конкретных флагов
if (state.registrationProcess.state === 'processing') {
// Блокируем кнопки
// Активируем спиннеры
}
if (state.registrationProcess.state === 'failed') {
// Выводим сообщение об ошибке
}
Кроме таких состояний, есть различные данные, сопровождающие наш процесс. Например, processed может завершиться с ошибками. В таком случае можно ввести дополнительно массив (или объект, в зависимости от структуры) с ошибками, который будет заполняться при их наличии:
const state = {
registrationProcess: {
errors: ['Имя не заполнено', 'Адрес имеет неверный формат'],
state: 'processed',
}
};
Причем этот же массив с ошибками удобно использовать для валидации формы до отправки на сервер. То есть будучи в состоянии filling.
А что, если мы захотим блокировать возможность отправки формы до того момента, пока не пройдет валидация на фронтенде? Есть два подхода: либо мы проверяем, что errors пуст, либо, что лучше, мы вводим явное состояние валидности формы. И тогда состояние нашего приложения становится таким:
const state = {
registrationProcess: {
errors: ['Имя не заполнено', 'Адрес имеет неверный формат'],
state: 'processed',
validationState: 'invalid' // или valid
}
};
В некоторых ситуациях возможно объединение, когда процесс валидации соединен с процессом обработки самой регистрации. Тогда вместо отдельного состояния validationState, появится дополнительное состояние invalid внутри state. Это не совсем корректно с точки зрения моделирования (потому что у нас действительно два разных процесса), но иногда такой способ позволяет написать чуть более простой код (до тех пор пока различий не станет много).
Глобально, такой подход в разработке называется программированием с явным выделенным состоянием. Он сводится к тому, что в рамках приложения находятся базовые процессы, от которых зависит все остальное. Затем эти процессы моделируются с помощью конечных автоматов (FSM). Причем не важно, какие инструменты используются для разработки: чистый DOM, jQuery или любой мощный современный фреймворк. Он применим везде и везде нужен.
Это невероятно мощная парадигма программирования, которая описана в книге "Автоматное Программирование" в наших рекомендациях.
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.