Многие вставали перед выбором той или иной библиотеки для работы с формами в 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 поле передающееся вторым аргументом в функцию валидатор, в нём мы передаём полученные с сервера ошибки после сабмита
К сожалению, я не встречал подобной или похожей реализации валидации, хотя она кажется очевидной: у нас есть функция валидации которая получает данные и возвращает на их основе ошибки, никакой side effect логики, всё так, как должно быть в реакте.
Перейдём к компоненту Transform. Он перехватывает изменения у вложенных полей и вызывает функцию — transformer, принимающую три аргумента:
- field — имя поля в котором произошло изменение
- value — новое значение этого поля
- model — текущее значение полей формы с предыдущим значением изменившегося поля
Она должна вернуть объект вида { [field]: value } который будет использован для обновление других полей формы.
Тоже очевидная реализация вычисляемых полей.
И… что мы имеем в итоге?
Так как изначально мы использовали декларативный подход и композицию мы можем комбинировать компоненты валидации и трансформации и использовать их для отдельных групп полей.
В компоненте Form отсутствуют лишние пропы отвечающие за дополнительный функционал (трансформация и валидация).
Field получает своё значение, информацию об ошибках и колбек функции по средствам контекстов, что позволяет создавать дополнительные компоненты для работы с формой и делегировать ответственность. Сама форма имеет вид схожий с html проекцией, что упрощает понимание.
react-painlessform
Я написал собственную библиотеку, которая помогает делать формы просто и понятно. С кодом можно ознакомиться на Github.
А так же посмотреть живой пример из статьи.
Спасибо за внимание
Раз вы дочитали до конца, то вы сильно любите формы или статья была интересна, я буду рад прочитать комментарии и услышать ваше мнение по поводу форм в реакте.
Автор: Wroud