React-формы, библиотека для частичной подписки

—

от автора

Что такое библиотека для работы с формами?

Это библиотека, которая обеспечивает обновление состояния 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


ссылка на оригинал статьи https://habr.com/ru/articles/587640/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *