Структура против хаоса - элегантное решение для создания форм в React.js
«Ну вот, опять эти формы...» — знакомая мысль? Мы постоянно ищем способ сделать их удобными и предсказуемыми, но идеальное решение все никак не находится. В этой серии статей Артем Леванов, Front Lead в WebRise, подробно разберет, с какими сложностями мы сталкиваемся, изучим разные подходы и в итоге придем к элегантному решению: как описывать все формы на сайте, используя всего по одному компоненту для каждого типа полей.
Проблемы кастомных React форм
Когда разработчик впервые сталкивается с задачей создать форму в React, всё кажется довольно простым: пара input, пара стейтов и обработчик submit.
Но чем больше полей и логики появляется, тем быстрее «игрушечное» решение превращается в гору кода, которую тяжело поддерживать. Вот простая форма логина. Пара input, к ним обработчики useState, перед отправкой запуск валидации. Все логично, просто и понятно.
Через какое-то время нас попросят сделать форму регистрации, где уже 10-20 полей. Что нам предстоит:
- написать для каждого поля свой useState;
- написать много if для проверки каждого поля на валидность;
- написать для каждого поля свое отображение ошибок.
В итоге файл формы может занимать сотни строк и мы сталкиваемся с очевидными проблемами:
- сложно ревьюить такую большую форму;
- легко допустить ошибку при написании кода (много однотипных действий и можно забыть например отрендерить ошибку для нового поля);
- невозможно быстро обучить новых разработчиков, потому что у каждого своя логика «как писать формы».
Так же, очевидной проблемой будет повторное использование форм. Допустим, у нас есть форма регистрации и форма профиля. Поля вроде «email» и «пароль» в них одинаковые, но код приходится дублировать. В результате:
- одно и то же поле реализовано в двух местах;
- правила валидации могут различаться;
- стили и UX становятся непоследовательными.
Это создает ситуацию, когда одинаковые поля ведут себя по разному и где истина непонятно.
Когда ручной код становится неуправляемым, разработчики ищут способы унификации работы с формами. В целом у нас три пути. Написать собственный велосипед, подключить готовые библиотеки, попытаться найти золотую середину, сочетая библиотеки и собственные слои абстракции. Первая реакция на хаос в коде — это желание спрятать его под универсальный компонент. И это приводит к созданию собственных франкенштейнов. Как пример компонент, который является и простым input, input c маской, селектом, обрабатывает ошибки, видимость поля:
На первый взгляд универсально, на практике же компонент перегружен условиями, его сложно поддерживать и тестировать, со временем даже автору будет сложно разобраться в его работе. И кажется теперь, что где-то мы свернули не туда с универсальностью и собственными решениями. Мы тоже так подумали и решили, что есть же уже готовые решения (React Hook Form, Formik), давайте возьмем их.
Популярные решения решают сразу несколько проблем:
- упрощает работу со стейтом и валидацией;
- экономят код, дают хорошую производительность;
- требуют меньше времени на обучение новых разработчиков работе с кодом наших форм.
Однако тут есть проблема, библиотеки помогают упаковать логику, но не ui. Вот пример формы с полями email и password и библиотекой React Hook Form.
У нас ушла пачка useState, валидации теперь прописываются в компоненте, мы сильно сократили верхнюю часть, относительно первой формы. Но что делать с выводом ошибок и дизайном каждого поля? Упакуем в отдельный компонент и в название добавим приписку Field. Так мы получим InputField, который уже можно переиспользовать в разных формах
В итоге получаем достаточно универсальный компонент для использования в разных формах
Уже не плохо, но мы все еще можем столкнуться с проблемами. Например нам будет сложно использовать InputField вне тега form, для каждого поля мы будем дублировать логику обработки ошибок, нет возможности внести даже небольшие изменения в дизайн поля.
Наше решение
Ощутив проблемы выше мы пришли к пониманию необходимости примитивов, на основе которых легко строить поля различных типов.
Примитивы - это маленькие переиспользуемые компоненты, которые:
- ничего не знают про react-hook-form или бизнес-логику;
- отвечают за внешний вид и базовое поведение;
- легко расширяются (например за счет тем для поддержки разных стилей).
Вот пример простого примитива Input:
Нет ничего лишнего только код самого поля. Такую часть можно легко вставить в любую систему при этом она будет иметь уже нужный нам вид.
Обертку, в которую мы вставляем наши примитивы мы назвали ячейкой Cell.
Она отвечает за единый дизайн поля, вывод ошибок и сопутствующей информации (подписи, примечания).
Совмещая ячейку и примитивные поля, мы формируем уже непосредственный компонент формы, который работает с логикой react-hook-form.
На каждый тип поля, который есть в html, мы создаем отдельный компонент TextField, TextareaField, SelectField, CheckboxField, RadioButtonField, FileField, плюс отдельно выделяем компонент под поля с масками MaskedField. Таким образом перекрываем все возможные поля, которые могут понадобиться при разработке форм.
Как результат, простое использование полей без необходимости, что либо прокидывать. Заполняем только основные атрибуты, все остальное делает компонент внутри.
Заключение
Работа с формами в React традиционно сопряжена с рядом трудностей. Кастомная реализация часто приводит к созданию сложного и плохо поддерживаемого кода, а попытки разработать собственное решение заканчиваются перегруженными компонентами. Даже популярные библиотеки, решая проблемы логики, оставляют открытыми вопросы единого интерфейса и повторного использования компонентов.
Представленный подход предлагает решение этих проблем через систему простых примитивов и типовых полей. Базовые примитивы, такие как Input и Cell, обеспечивают единообразие дизайна и поведения, на основе которых строятся конкретные поля — TextField, MaskField, RadioField и другие. Это позволяет собирать формы из готовых, отлаженных блоков, а не писать их с нуля каждый раз.
В результате разработчики получают читаемый код, где каждое поле представлено небольшим самостоятельным компонентом, а не сотнями строк условной логики. Это обеспечивает единый UX/UI для всех форм, что гарантирует консистентность и упрощает работу дизайнеров и тестировщиков. Компоненты легко переиспользуются не только в формах, но и в других сценариях, таких как поиск, а система остаётся гибкой и простой для масштабирования.
Со стороны бизнеса это означает повышение скорости разработки. Создание новых форм ускоряется за счёт готовых решений, покрывающих до 90% потребностей. Надёжность системы возрастает, снижая количество ошибок при внесении изменений или добавлении новых полей. Пользователи получают предсказуемый и интуитивно понятный опыт взаимодействия, а новые члены команды быстрее входят в проект, осваивая единую библиотеку компонентов вместо разбора множества кастомных реализаций.
По вопросам, телеграм @webrise1