Современные фронтенд-приложения включают множество элементов, которые должны корректно реагировать на изменения данных: спиннеры крутятся, кнопки отключаются, данные отправляются. Управление этим процессом требует продуманного подхода. В идеале, любые изменения в интерфейсе являются следствием изменения данных, то есть состояния приложения. Представьте себе форму регистрации, у которой кнопка отправки (submit) заблокирована во время выполнения запроса на сервер (с точки зрения UX это обязательно для любых форм). В таком случае состояние может приобрести следующий вид:
const state = {
registrationProcess: {
valid: true,
submitDisabled: true,
isLoading: true,
},
};
Флаг submitDisabled
отвечает за то, будет ли кнопка заблокирована. Она блокируется во время отправки формы и разблокируется если пришел ответ с ошибками для возможности повторной отправки. Если отправка прошла успешно, то вместо формы покажется что-то еще и этот флаг перестанет использоваться до появления новой формы.
В реальных приложениях все еще сложнее. Во время отправки данных блокируется не только кнопка отправки, но и поле для ввода. Более того, отправка данных в одном месте, может повлиять и на остальные блоки на странице, которые могут пропадать, блокироваться или видоизменяться. Не говоря уже о том, что причин блокировки кнопки может быть несколько. Она может быть заблокирована просто потому, что в форму введены некорректные данные.
Если решать эту задачу в лоб, получится состояние с большим количеством флагов, где каждый флаг отвечает за какой-то свой элемент на странице.
const state = {
registrationProcess: {
valid: true,
submitDisabled: true,
inputDisabled: true,
showSpinner: true,
blockAuthentication: true,
},
};
Но такой подход очень быстро усложняет разработку. Когда состояние описывается через множество независимых флагов, может возникнуть логическая путаница. Например, если форма валидна (valid: true
), но submitDisabled: true
, пользователь не сможет отправить ее, хотя логически это должно быть возможно. При увеличении числа таких флагов усложняется понимание взаимосвязей и проверок.
Также усложнится логика вывода, поскольку внешний вывод начнет зависеть от различных комбинаций флагов. Становится сложнее поддерживать и предсказывать, какие элементы интерфейса должны отображаться в разных состояниях. Разберем несколько примеров.
// Логика вывода в UI
const showSubmitButton =
state.registrationProcess.valid && !state.registrationProcess.submitDisabled;
const showSpinner = state.registrationProcess.showSpinner;
const disableInput =
state.registrationProcess.inputDisabled ||
state.registrationProcess.blockAuthentication;
console.log({ showSubmitButton, showSpinner, disableInput });
// { showSubmitButton: false, showSpinner: true, disableInput: true }
Проблема данного подхода в том, что он опирается не на причины происходящего, а на их следствия. Изменение активности кнопки, блокирование элементов, отображение спиннеров — все это следствия каких-то процессов. Умение выделить эти процессы и правильно описать в состоянии, один из краеугольных камней хорошей архитектуры.
В примере выше большая часть флагов связана с процессом обработки данных формы. Предположим, что после отправки формы, данные уходят на сервер, затем от него приходит ответ и дальше результат отображается пользователю. Результат может быть как успешным, так и не успешным. Мы должны продумывать все исходы. Весь процесс условно можно разбить на несколько промежуточных состояний:
Предложенный набор не является универсальным. Процессы могут быть устроены сложнее, а значит потребуется другой набор состояний.
- filling – заполнение формы. В этом состоянии все активно и доступно для редактирования.
- processing (или sending) – отправка формы. Это то самое состояние, когда пользователь ждет, а приложение пытается предотвратить нежелательные действия, например, клики или изменения данных формы.
- processed (или finished) – состояние, обозначающее, что все завершилось. В нем форма уже не отображается.
- failed – состояние, обозначающее завершение с ошибкой. Например, произошел сбой в сети во время загрузки или загруженные данные оказались неверными.
Использование одного свойства state
вместо нескольких флагов упрощает логику. Например, состояние processing
автоматически определяет, что форма заблокирована, спиннер активен, а кнопка submit
отключена — и для этого не нужно держать три отдельных флага. Такой подход снижает вероятность багов и делает код более читаемым.
Перепишем наше состояние убрав оттуда все флаги и введя одно свойство отвечающее за состояние работы с формой:
const state = {
registrationProcess: {
state: "filling", // 'processing', 'processed', 'failed'
},
};
В состоянии мы выделили процесс регистрации, который может принимать одно из возможных состояний. Даже такое, на первый взгляд, небольшое изменение резко упрощает систему. Теперь нам не нужно отслеживать каждый участвующий в этом процессе элемент. Главное, чтобы все возможные состояния описывали все возможные варианты поведения. Тогда все проверки в выводе сведутся к проверке общего состояния:
// Этих "ифов" может быть сколько угодно,
// главное, что они завязаны на общее состояние, а не проверку конкретных флагов
if (state.registrationProcess.state === "processing") {
// Блокируем кнопки
// Активируем спиннеры
}
if (state.registrationProcess.state === "failed") {
// Выводим сообщение об ошибке
}
Кроме таких состояний, есть различные данные, сопровождающие наш процесс. Например, processed может завершиться с ошибками. В таком случае можно ввести дополнительно массив (или объект, в зависимости от структуры) с ошибками, который будет заполняться при их наличии:
const state = {
registrationProcess: {
errors: ["Имя не заполнено", "Адрес имеет неверный формат"],
state: "failed",
},
};
Причем этот же массив с ошибками удобно использовать для валидации формы до отправки на сервер. То есть будучи в состоянии filling.
А что, если мы захотим блокировать возможность отправки формы до того момента, пока не пройдет валидация на фронтенде? Есть два подхода: либо мы проверяем, что errors пуст, либо, что лучше, мы вводим явное состояние валидности формы. И тогда состояние нашего приложения становится таким:
const state = {
registrationProcess: {
errors: ["Имя не заполнено", "Адрес имеет неверный формат"],
state: "processed",
validationState: "invalid", // или valid
},
};
В некоторых ситуациях возможно объединение, когда процесс валидации соединен с процессом обработки самой регистрации. Тогда вместо отдельного состояния validationState
, появится дополнительное состояние invalid
внутри state
. Это не совсем корректно с точки зрения моделирования (потому что у нас действительно два разных процесса), но иногда такой способ позволяет написать чуть более простой код (до тех пор пока различий не станет много).
Глобально, такой подход в разработке называется программированием с явным выделенным состоянием. Он сводится к тому, что в рамках приложения находятся базовые процессы, от которых зависит все остальное. Причем не важно, какие инструменты используются для разработки: чистый DOM, jQuery или любой мощный современный фреймворк. Он применим везде и везде нужен.
Собирая все вместе
Пример ниже демонстрирует этот подход на простой форме регистрации.
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<title>Форма с явным состоянием</title>
</head>
<body>
<form id="registrationForm">
<input type="email" id="emailInput" placeholder="Введите email" />
<button type="submit" id="submitButton" disabled>Отправить</button>
<div id="message"></div>
</form>
<script>
const state = {
registrationProcess: {
state: "filling", // "processing", "failed", "success"
errors: [],
},
};
const form = document.getElementById("registrationForm");
const emailInput = document.getElementById("emailInput");
const submitButton = document.getElementById("submitButton");
const message = document.getElementById("message");
function validateEmail(email) {
return email.trim() !== ""; // Простая проверка: email не должен быть пустым
}
function updateUI() {
const { state: processState, errors } = state.registrationProcess;
if (processState === "processing") {
submitButton.disabled = true;
message.textContent = "Отправка...";
} else if (processState === "failed") {
submitButton.disabled = false;
message.textContent = `Ошибка: ${errors.join(", ")}`;
} else if (processState === "success") {
submitButton.disabled = true;
message.textContent = "Успешно отправлено!";
} else {
submitButton.disabled = state.registrationProcess.errors.length > 0;
message.textContent = "";
}
}
emailInput.addEventListener("input", () => {
const email = emailInput.value;
state.registrationProcess.errors = validateEmail(email)
? []
: ["Некорректный email"];
updateUI();
});
form.addEventListener("submit", (event) => {
event.preventDefault();
if (state.registrationProcess.errors.length > 0) return;
state.registrationProcess.state = "processing";
updateUI();
setTimeout(() => {
if (validateEmail(emailInput.value)) {
state.registrationProcess.state = "success";
} else {
state.registrationProcess.state = "failed";
state.registrationProcess.errors = ["Ошибка при отправке"];
}
updateUI();
}, 2000);
});
updateUI();
</script>
</body>
</html>
Разбор кода
Явное состояние
state.registrationProcess.state
filling
– ввод данных в поле.processing
– отправка формы, блокировка кнопки и поля.success
– успешная регистрация, кнопка отключается.failed
– ошибка валидации, отображается сообщение.
Функция updateUI()
- Управляет блокировкой кнопки и полем ввода.
- Показывает сообщения "Отправка...", "Ошибка..." или "Успешно отправлено!".
- Блокирует кнопку, если валидация не пройдена.
Валидация email при вводе
- Если
email
пуст, кнопкаSubmit
остается заблокированной. - Ошибка отображается сразу, без необходимости отправки.
- Если
Отправка формы (
submit
обработчик)- При клике кнопка блокируется, состояние "processing".
- Через
setTimeout()
эмулируется серверный ответ. - Если email валиден →
success
, иначе →failed
.
Это невероятно мощная парадигма программирования, которая описана в книге "Автоматное Программирование" в наших рекомендациях.
Дополнительные материалы

Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.