
Многие вставали перед выбором той или иной библиотеки для работы с формами в ReactJS. Когда я выбирал подходящую мне, разные библиотеки казались идеальными НО: форма на конфигах или колбеки в onSubmit эвенте, или асинхронный submit. Почему формы для реакта не соответствуют принципам реакта, почему они выглядят как что-то особенное? Если эти вопросы приходили вам в голову, или вы любите формы, приглашаю к прочтению статьи.
Давайте представим формы какими они должны быть.
Форма в реакте должна:
- предоставлять управляемость полей и событий
- максимально соответствовать html проекции
- соблюдать декларативность и композицию
- использовать типичные методы работы с React компонентами
- иметь предсказуемое поведение
Форма в реакте не должна:
- задавать модель управления
- иметь избыточное состояние или требовать дополнительные данные
- требовать настройку или обязательное использование функций-хелперов
Теперь попробуем описать идеальную форму опираясь на эти правила:
<Form action="/" method="post"> <Validation> <Field type="text" name="firstName"> <Field type="text" name="lastName"> <Transform> <Field type="number" name="minSalary"> <Field type="number" name="maxSalary"> <Transform> <Field type="email" name="email"> </Validation> <button type="submit">Send<button> </Form>
Выглядит практически как обычная html форма, за исключением Field вместо input и неизвестных Validation и Transform. Вы, наверное, уже догадались что тег Validation должен проверять значение полей и создавать сообщения ошибки для них. Тег Transform в свою очередь необходим для вычисления полей minSalary и maxSalary.
Я что-то говорил про React?
Перенесёмся в реалии реакта и опишем ту же форму:
class MySexyForm extends React.Component { constructor(props) { super(props); this.state = { model: {} }; this.validator = (model, meta) => { let errors = { ...meta.submitErrors }; if(model.firstName && model.firstName.length > 2) { errors = { firstName: ["First name length must be at minimum 2"] }; } if(model.lastName && model.lastName.length > 2) { errors = { ...errors, lastName: ["Last name length must be at minimum 2"] }; } return errors; }; this.transformer = (field, value, model) => { switch (field) { case "minSalary": if (parseInt(value) > parseInt(model.maxSalary)) { return { maxSalary: value }; } case "maxSalary": if (parseInt(value) < parseInt(model.minSalary)) { return { minSalary: value }; } } return {}; }; } onSubmit = (event) => (model) => { event.preventDefault(); console.log("Form submitting:", model); this.props.sendSexyForm(model); // абстрактный action после выполнения которого в форму приходят ошибки сабмита в виде submitErrors пропа } onModelChange = (model) => { console.log("Model was updated: ", model); this.setState({ model }); } render() { return ( <Form action="/" method="post" onSubmit={this.onSubmit} onModelChange={this.onModelChange} values={this.state.model} initValues={this.props.initValues} > <Validation validator={this.validator} submitErrors={this.props.submitErrors}> <Field type="text" name="firstName"> <Field type="text" name="lastName"> <Transform transformer={this.transformer}> <Field type="number" name="minSalary"> <Field type="number" name="maxSalary"> <Transform> <Field type="email" name="email"> </Validation> <button type="submit">Send<button> </Form> ); } };
Я не стану подробно рассматривать Field компонент, представим что он рендерит input с переданными в Field пропами и дополнительными value и onChange. А так же сообщения об ошибках для данного поля.Стоит объяснить появление новых полей initValues, values, onModelChange, onSubmit, validator, transformer.
Начнём с пропов добавленных в Form.
Эвент хендлер onSubmit позволяет перехватить эвент сабмита формы, получить доступ к этому эвенту и к текущим значениям полей формы через аргумент model.
Эвент хендлер onModelChange позволяет отследить изменения в полях формы.
С помощью values мы можем управлять значениями полей, а initValues позволяет задать начальные значения.
Этот базовый функционал предоставляет большинство библиотек для работы с формами в реакте, ничего необычного, всё так как должно быть.
Рассмотрим тег Validation, у него появились два пропа
- validator — функция возвращающая ошибки валидации на основе переданных значений полей формы
- submitErrors — дополнительное rest поле передающееся вторым аргументом в функцию валидатор, в нём мы передаём полученные с сервера ошибки после сабмита. По сути в rest передаются все аргументы переданные в Validation за исключением validator
К сожалению, я не встречал подобной или похожей реализации валидации, хотя она кажется очевидной: у нас есть функция валидации которая получает данные и возвращает на их основе ошибки, никакой side effect логики, всё так, как должно быть в реакте.
Перейдём к компоненту Transform. Он перехватывает изменения у вложенных полей и вызывает функцию — transformer, принимающую три аргумента:
- field — имя поля в котором произошло изменение
- value — новое значение этого поля
- model — текущее значение полей формы с предыдущим значением изменившегося поля
Она должна вернуть объект вида { [field]: value } который будет использован для обновления других полей формы.
Тоже очевидная реализация вычисляемых полей.
И… что мы имеем в итоге?
Так как изначально мы использовали декларативный подход и композицию мы можем комбинировать компоненты валидации и трансформации и использовать их для отдельных групп полей.
В компоненте Form отсутствуют лишние пропы отвечающие за дополнительный функционал (трансформация и валидация).
Field получает своё значение, информацию об ошибках и колбек функции по средствам контекстов, что позволяет создавать дополнительные компоненты для работы с формой и делегировать ответственность. Сама форма имеет вид схожий с html проекцией, что упрощает понимание.
react-painlessform
Я написал собственную библиотеку, которая помогает делать формы просто и понятно. С кодом можно ознакомиться на Github.
А так же посмотреть живой пример из статьи.
Спасибо за внимание
Раз вы дочитали до конца, то вы сильно любите формы, или статья была интересна, я буду рад прочитать комментарии и услышать ваше мнение по поводу форм в реакте.
