Что такое библиотека для работы с формами?
Это библиотека, которая обеспечивает обновление состояния react приложения в зависимости от введенных пользователем данных.
Что значит обеспечивает обновления состояния?
Когда пользователь вводит какие-то данные в <input/>
, библиотека сохраняет новые данные и оповещает о введенных данных остальные части react-приложения. По сути библиотека передает в <input/>
собственные value и onChange, чтобы контролировать состояние компонента.
Так в чем проблема?
Есть два подхода к механизму подписки:
Оповещение всех компонентов, которые используют какие-либо данные формы (глобальная подписка)
Оповещение компонентов использующих только те поля, которые изменились (частичная подписка)
Вариант с глобальной подпиской прост в реализации и использовании, вариант с подпиской на определенные поля производительнее поскольку не заставляет обновлять компоненты без необходимости. Эти подходы напоминают redux(имеет глобальную подписку) и mobx(подписывается на изменения определенных переменных).
Если вы будете выбирать библиотеку для работы с формами, то заметите что большинство библиотек реализуют глобальную подписку(formik, final-form). Это простой подход, который, однако, увеличивает число обновлений компонентов. Для приложения с большим количеством полей на странице это становится краеугольным камнем производительности.Получается что вводя какой-либо символ в input, мы обновляем все наше react дерево(иногда несколько раз).
Механизм частичной подписки реализован в данный момент в относительно небольшом количестве библиотек(react-hook-form, mobx-react-form). Каждая из известных мне библиотек этого типа имеет изъян. Для примера: mobx-react-form зависит от mobx и не может работать без него, react-hook-form имеет неточную документацию, неудобный api для множественной подписки и самое главное механизм работы с input-ами посредством работы напрямую с DOM-узлами(инпуты становятся stateless, что ведет к множеству багов).
По итогам я не нашел библиотеку, которая реализовывала бы частичную подписку и была бы достаточно удобна в использовании api.
Давайте представим!
Теперь представим что ни одна существующая библиотека нам не подходит и мы решили написать свою, какой она должна быть?
Она должна реализовывать частичные подписки
Она должна иметь удобное api, схожее с популярными библиотеками(для меня это formik)
Она должна быть интегрирована с популярными средствами валидации
Подписка
В formik-е мы работаем с данными примерно так:
const [nameField] = useField('name');
Здесь все достаточно понятно, посредством useField мы подписываемся на изменения формы и получаем данные поля. В случае formik-a мы подписываемся не на конкретное поле, на любое изменение в форме, получая при этом значение для конкретного поля. Но поскольку useField имеет имя поля в качестве параметра, он может реализовать частичную подписку. Реализуем механизм подписки на стороне библиотеки, поскольку react context не умеет частично обновлять зависимости:
// отдельный массив подписчиков для каждого поля формы
class Field {
value: ''
// ...
listeners: []
// ..
}
При этом, кажется, можно отказаться от необходимости деструктуризации данных. В случае formik-a получения всех данных о поле будет выглядеть вот так:
const [nameField, nameMeta, nameHelpers] = useField('name');
return (
<div>
<input {...nameField} />
<div>{nameMeta.error}</div>
<button
onClick={() => {
nameHelpers.setValue('');
}}
>
clear field
</button>
</div>
);
Однако деструктуризация заставляет нас писать больше кода и следить за тем получили ли мы из деструктуризации необходимое нам свойство поля, сделаем так, чтобы useField возвращал объект со всеми этими свойствами:
const nameField = useField('name');
return (
<div>
<input {...nameField.getInputProps()} />
<div>{nameField.error}</div>
<button
onClick={() => {
nameField.set('');
}}
>
clear field
</button>
</div>
);
(здесь использован метод `getInputProps()`, поскольку как кажется, он лучше описывает то что происходит при передаче данных в input и дает возможность использовать другие варианты компоновки в будущем).
В целом улучшения незначительные, но с ростом количества используемых полей положительный эффект становится заметней:
formik
const [nameField, nameMeta, nameHelpers] = useField('name');
const [ageField, ageMeta, ageHelpers] = useField('age');
const [addressField, addressMeta, adressHelpers] = useField('address');
const [favoriteGameField, favoriteGameMeta, favoriteGameHelpers] =
useField('favoriteGame');
const [bankAccountField, bankAccountMeta, bankAccountHelpers] =
useField('bankAccount');
Новая библиотека:
const nameField = useField('name');
const ageField = useField('age');
const addressField = useField('address');
const favoriteGameField = useField('favoriteGame');
const bankAccountField = useField('bankAccount');
Представьте сколько времени можно потратить на написание и отслеживание переменных в версии formik-a.
API схожее с formik-ом
useField
возвращает сущность, а как работать с данными глобально?
formik имеет свойства и методы для самой формы:
setValues()
setErrors()
setTouched()
setFieldValue()
setFieldError()
setFieldTouched()
values
errors
touched
Сделаем похожее api
setValues()
setErrors()
setTouches()
setValue()
setError()
setTouched()
values
errors
touches
(изменено слово touched на слово touches, что отражает множественное число, убрано лишнее упоминание Field).
Валидация
И последнее, валидация. Formik имеет удобный механизм работы с валидацией(yup), просто сделаем такой-же в новой библиотеке:
const form = useForm({
validateSchema: yup.object({
name: yup.string().required(),
}),
});
Так же в дополнение к этому можно добавить использование unstable_batchedUpdates внутри библиотеки для повышения производительности.
Кажется все готово. Как было и даже лучше, с частичной подпиской.
Авторские улучшения
Но когда начал что-то менять сложно остановиться, и вот несколько улучшений, которые хотел бы добавить я.
В formik-е нет деления на логические сущности и все методы исходят именно из этого. Так, например, выставление несколько свойств для одного поля в formik-е будет выглядеть вот так:
form.setFieldValue('name', 'anon');
form.setFieldError('name', 'you are anon!!!');
form.setFieldTouched('name', true);
А при наличии отдельной структуры Field может выглядеть вот так:
const nameField = form.fields.name;
nameField.set('anon');
nameField.setError('you are anon!!!');
nameField.setTouched(true);
Кажется неплохое улучшение, исчезает необходимость писать название поля несколько раз.
Есть два типа людей
Любое множество можно разделить на две части посредством "не". Вот и я, автор, один из тех людей, кто не любит лишний раз доставать значение из сущности в отдельную переменную. Следующий подраздел о для таких людей как я и никак не влияет на правильность рассуждений, которые были до этого.
Как можно упросить этот код?
const nameField = useField('name');
const ageField = useField('age');
const addressField = useField('address');
const favoriteGameField = useField('favoriteGame');
Довольно просто, не доставать значения из переменных, а работать напрямую с формой:
const form = useForm('name', 'age', 'address', 'favoriteGame');
Насколько это удобно каждый решает сам, но лично для меня намного удобней подписываться на форму с помощью механизма глобальной формы:
const form = useForm('name');
return <input {...form.fields.name.getInputProps()} />;
Немного безопасности
Когда работаешь с глобальной формой велик риск изменить поле на которое ты не подписан. Для этого в нашей воображаемой библиотеке в режиме разработки должен быть proxy объект, который будет замещать форму. Он будет отслеживать изменения и оповещать о попытке изменения поля в компоненте, который на это поле не подписан.
const form = useForm('name');
return (
<button onClick={() => form.fields.age.set('')}>
clear name
</button>
);
В данном случае в режиме разработки мы получим ошибку
`You don't have access to field - age`
Пару слов о синхронности
Что будет находиться в значении поля name, если написать такой код для formik-а?
formik.setFieldValue('name', 'robbin');
console.log(formik.values.name); // previous name
Правильный ответ - значение, которое находилось в name до вызова setFieldValue. Это нельзя назвать явной ошибкой, как минимум такой подход используется в react, однако на мой взгляд это не самое очевидное поведение и другое поведение было бы логичнее:
form.setValue('name', 'robbin');
console.log(form.values.name); // robbin
В итоге
Соберем все вместе!
// app.js
function App() {
const form = useInitNewForm({
validateSchema: yup.object({
name: yup.string().required(),
age: yup.number().required(),
}),
});
return (
<NewFormProvider value={form}>
<div>
<Name />
<Age />
</div>
</NewFormProvider>
);
}
// name.js
function Name() {
const form = useForm('name');
return <input {...form.fields.name.getInputProps()} />;
}
// age.js
function Age() {
const ageField = useField('age');
return <input {...ageField.getInputProps()} />;
}
Пройдя цепочку рассуждений выше, я осознал что такую библиотеку стоит создать. В ней реализованы:
частичная подписка
formik-подобное api
удобная множественная подписка
proxy-объект который в dev режиме оборачивает форму и позволяет отслеживать неправомерный доступ
batch-ing обновлений компонентов посредством unstable_batchedUpdates
Как кажется получилось довольно лаконично и производительно. Хотелось бы услышать ваши отзывы и предложения.
→ GitHub
→ Sandbox