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

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

Комментарии (0)