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

Состояние форм JS: Архитектура фронтенда

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

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

Неконтролируемые формы

Неконтролируемые формы — это подход, при котором состояние формы хранится внутри самой формы и извлекается только при её отправке. Это привычный способ работы с формами вне фреймворков:

form.addEventListener("submit", (e) => {
  const formData = new FormData(e.target);
  // Обработка данных, например, отправка на сервер
});

К достоинствам этого способа относят:

  • Простоту. Мало кода, не нужно хранить состояние.
  • Скорость. Браузер делает всю работу сам. Минимум вмешательства со стороны пользовательского кода.

Несмотря на легкость и очевидность, этот подход обладает одним недостатком. При таком подходе невозможно реагировать на изменения формы в процессе ее заполнения. Где это может быть нужно? Вот несколько примеров:

  • Автодополнение. Выпадающие списки зависят от того, что было набрано.
  • Валидация в процессе набора. Часто реализуется в виде красной рамки вокруг поля для ввода.
  • Моментальная фильтрация. Такое часто используется на сервисах бронирования или поиска товаров. Достаточно выбрать какой-то пункт меню, как сразу же меняется выборка.

В такой ситуации нам понадобятся контролируемые формы.

Контролируемые формы

Контролируемые формы предполагают хранение состояния всех элементов формы в приложении. Любое изменение в форме отслеживается, анализируется и отражается в состоянии приложения, что позволяет мгновенно реагировать на действия пользователя.

Для реализации такого поведения нам понадобится добавить в состояние структуру отражающую форму. Сделаем это для формы регистрации включающей в себя имя, email и пароль.

const state = proxy({
  formData: {
    name: "",
    email: "",
    password: "",
  },
  registrationProcess: {
    state: "filling", // "processing", "failed", "success"
    errors: [],
  },
});

Теперь посмотрим на весь код целиком

<!doctype html>
<html lang="ru">
  <head>
    <meta charset="UTF-8" />
    <title>Контролируемая форма регистрации с Valtio</title>
    <script type="importmap">
      {
        "imports": {
          "valtio": "https://esm.sh/valtio"
        }
      }
    </script>
  </head>
  <body>
    <form id="registrationForm">
      <input type="text" id="nameInput" placeholder="Введите имя" />
      <input type="email" id="emailInput" placeholder="Введите email" />
      <input type="password" id="passwordInput" placeholder="Введите пароль" />
      <button type="submit" id="submitButton">Зарегистрироваться</button>
      <div id="message"></div>
    </form>

    <script type="module">
      import { proxy, subscribe, snapshot } from "valtio/vanilla";

      const state = proxy({
        formData: {
          name: "",
          email: "",
          password: "",
        },
        registrationProcess: {
          state: "filling", // "processing", "failed", "success"
          errors: [],
        },
      });

      const form = document.getElementById("registrationForm");
      const nameInput = document.getElementById("nameInput");
      const emailInput = document.getElementById("emailInput");
      const passwordInput = document.getElementById("passwordInput");
      const submitButton = document.getElementById("submitButton");
      const message = document.getElementById("message");

      function validateForm() {
        const errors = [];

        if (state.formData.name.trim().length < 2) {
          errors.push("Имя должно содержать минимум 2 символа");
        }

        if (!state.formData.email.includes("@")) {
          errors.push("Некорректный email");
        }

        if (state.formData.password.length < 6) {
          errors.push("Пароль должен содержать минимум 6 символов");
        }

        state.registrationProcess.errors = errors;
      }

      function updateUI() {
        const obj = snapshot(state);
        const { state: processState, errors } = obj.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 = false;
          message.textContent = errors.join(", ");
        }
      }

      subscribe(state, updateUI);

      nameInput.addEventListener("input", (e) => {
        state.formData.name = e.target.value;
        // Теперь можно выполнять какую-то дополнительную логику
      });

      emailInput.addEventListener("input", (e) => {
        state.formData.email = e.target.value;
        // Теперь можно выполнять какую-то дополнительную логику
      });

      passwordInput.addEventListener("input", (e) => {
        state.formData.password = e.target.value;
        // Теперь можно выполнять какую-то дополнительную логику
      });

      form.addEventListener("submit", (event) => {
        event.preventDefault();

        validateForm();

        if (state.registrationProcess.errors.length > 0) {
          state.registrationProcess.state = "failed";
          return;
        }

        state.registrationProcess.state = "processing";

        setTimeout(() => {
          if (state.formData.email !== "error@example.com") {
            state.registrationProcess.state = "success";
          } else {
            state.registrationProcess.state = "failed";
            state.registrationProcess.errors = ["Ошибка при отправке данных"];
          }
        }, 2000);
      });

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

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

Преимущества такого подхода:

  • Возможность мгновенной реакции на любые изменения формы.
  • Удобная реализация динамических интерфейсов (валидация, автодополнение, фильтрация).

И недостатки:

  • Больше кода и более сложная реализация.
  • Меньшая производительность из-за постоянного мониторинга состояния. Актуальность этого недостатка зависит от сложности формы и производительности приложения.

В популярных фреймворках (React, Vue, Angular) для реализации контролируемых форм существуют готовые инструменты (например, React Hook Form для React, Vuelidate для Vue, Reactive Forms для Angular). Они значительно упрощают работу с контролируемыми формами, предоставляя удобные API для валидации, управления состоянием и обработки событий.

Что использовать?

В чистом JS предпочтительнее неконтролируемые формы. Так намного проще и быстрее. И только в том случае, когда нужна мгновенная реакция, можно вводить контроль данных формы. Причем необязательно переходить от одного способа к другому целиком. Можно использовать гибрид, вводить контроль только тех данных, где без этого никак.


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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