Иногда мы работаем над проектом, в котором есть только несколько форм, и мы не хотим использовать какую-либо стороннюю библиотеку для обработки форм для этих простых случаев использования. Но если мы не будем использовать какую-либо библиотеку, то нам придется вручную обрабатывать каждую форму отдельно, что также не является хорошим подходом.

Итак, что мы должны делать тогда?

Вы правильно догадываетесь. 👍
Создайте универсальный пользовательский хук и используйте его для обработки каждой формы.

Необходимое условие:

Это будет безопасный крючок. Поэтому у вас должно быть правильное понимание TypeScript.

Ниже приведены ссылки на функции TypeScript, используемые в этой статье. Если вы еще этого не знаете, я настоятельно рекомендую сначала изучить их, прежде чем идти дальше.

  1. дженерики
  2. Виды утилит
  3. Сужение типов с помощью предикатов типов
  4. Оператор типа Keyof

Реализация:

Прежде чем начать, взгляните на окончательный интерфейс нашего хука, чтобы понять, что мы собираемся реализовать.

Управление состоянием:

Начнем с определения состояний. В этом хуке мы будем управлять четырьмя разными состояниями.

Здесь первые три состояния говорят сами за себя, что касается четвертого состояния, мы управляем им для определения того, было ли затронуто поле или нет, поскольку мы будем показывать ошибки только в затронутых полях. Он будет использоваться в событии onBlur, всякий раз, когда событие размытия срабатывает в любом поле, тогда true будет установлено для этого поля в состоянии.

Проверка:

Как упоминалось в заголовке, мы будем использовать очень популярную библиотеку под названием Yup для проверки вводимых пользователем данных.

Тип объекта проверки:
Вы видели выше, что мы принимаем validationSchema в качестве третьего аргумента нашего хука. Нам нужно, чтобы эта схема была безопасной для типов, то есть все поля формы должны быть определены в объекте схемы, и она не должна принимать поля, не являющиеся частью формы. Определим для него тип.

import { AnySchema} from "yup";
export type YupSchemaObject<T> = Record<keyof T, AnySchema>;

Здесь «T» является общим значением, и его тип будет зависеть от initialValues передачи потребителем. AnySchema — это тип объекта проверки, импортированный из yup. При использовании типа YupSchemaObject наша схема будет принимать только все ключи, определенные в объекте initialValues, и все значения в этой схеме будут соответствовать типу AnySchema. Например:

Если наши начальные значения таковы:

const initialValues = {
  name: '',
  email: ''
}

тогда в нашем validationSchema должны быть ключи name и email, и он не должен содержать никаких других ключей.
См. фрагмент кода ниже для лучшего понимания.

Функция проверки:
Надеюсь, вы поняли тип объекта проверки. Теперь пришло время создать функцию проверки.

Поскольку validationSchema является необязательным параметром, мы сначала проверяем его существование, а затем передаем его в yupObject, который импортируется из библиотеки yup. Далее мы просто передаем values и options в функцию schema.validate. Обратите внимание, что { abortEarly: false } здесь важно, потому что его значение по умолчанию равно true, и оно будет возвращать только первое поле ошибки, пока оно истинно, но нам нужно, чтобы оно возвращало все поля ошибок.

Сериализатор ошибок проверки:
я также хотел бы добавить в этот раздел служебную функцию, поскольку она является частью проверки, но будет использоваться позже в этом руководстве в разделе Размытиеи Отправить обработчики.

Эта функция принимает два параметра: первый — объект err, возвращаемый yup, а второй — touchedFields, который является необязательным. Чтобы понять реализацию этой функции, нам нужно знать, какова форма объекта ошибки yup. Итак, вот оно.

Все эти свойства являются частью типа ValidationError. Поскольку мы установили для параметра abortEarly значение false , мы получим свойство inner в виде массива ValidationError, что означает, что каждый элемент массива inner будет иметь объект типа ValidationError и содержать все вышеперечисленные свойства.

Теперь, когда мы знаем форму объекта ошибки, давайте начнем разбираться с реализацией нашего сериализатора.

Нам нужно преобразовать свойство inner, которое представляет собой массив объектов, в простой линейный объект с ключом Keyof Values и значением string. В объекте ошибки yup свойство path будет иметь имя поля, в котором возникает ошибка, а свойство message будет иметь текст ошибки в string. Нам просто нужно установить свойство path в качестве ключа нашего объекта ошибки и свойство message в качестве значения. Перед установкой значения ошибки каждого поля в наш объект состояния нам также необходимо проверить необязательный параметр touchedFields. Если он существует, нам нужно проверить состояние касания каждого поля, если состояние касания поля установлено только на true, тогда мы установим состояние ошибки этого поля, в противном случае мы будем игнорировать ошибку этого конкретного поля.

Обработчики событий:

Обработчики событий являются наиболее важной частью формы. Три общих обработчика, которые мы часто используем в форме, — это change, blur и submit handler.

Обработчик изменений:
Поскольку мы создаем обобщенный хук, который можно использовать для разных типов входных данных, нам нужно убедиться, что наш обработчик изменений обрабатывает все типы входных данных. Для этого нам нужно создать служебную функцию, которая будет обрабатывать значения в зависимости от типа ввода.

Мы передадим объект event.currentTarget в эту функцию, и она вернет значение в зависимости от типа ввода.

Этот обработчик может быть передан в ввод, текстовую область и поле выбора, поэтому мы создали тип с именем InputTypes и сделали его объединением трех. Но с этим типом объединения возникает проблема, заключающаяся в том, что мы не можем получить доступ к атрибутам files и checked event.currentTarget, поскольку эти атрибуты не существуют в полях textarea и select, а объединение трех сделало его недоступным для TypeScript. Чтобы решить эту проблему, мы преобразовали тип currentTarget в EventTarget & HTMLInputElement, потому что мы уверены, что если тип — «файл» или «флажок», то это будет вводполе, а не текстовая область или выберите поле. Наконец, мы устанавливаем входное значение в нашем состоянии, используя служебную функцию, которую мы создали ранее для обработки различных типов ввода.

Обработчик размытия:
этот обработчик будет выполнять две функции. Один из них — установить состояние касания (когда поле теряет фокус в первый раз), а другой — вызвать функцию проверки (определена в разделе проверки) и установить состояние ошибки поля в случае возникновения какой-либо ошибки.

Во-первых, мы проверяем, является ли значение поля уже истинным или нет в нашем объекте состояния touched, и если это не так, то мы устанавливаем его в true, а затем мы вызываем функцию validate. Поскольку это функция async и возвращает Promise, мы обрабатываем ее через блок then/catch. Если он отклоняется, то мы сериализуем ошибки yup через нашу функцию serializeYupError и устанавливаем ошибки в нашем состоянии, в противном случае мы устанавливаем пустой объект в состояние ошибки.

Обработчик отправки:
В его обязанности входит проверка всех значений формы, а затем вызов функции submitHandler (второй параметр хука) с values в качестве аргумента.

В блоке try мы вызываем функцию validate и awaited, так как это асинхронная функция. Если все наши проверки пройдены и эта функция не выдает ошибку, то наше состояние isSubmitting будет true и будет вызвана функция submitHandler.
Эта реализация submitHandler полностью зависит от потребителя этого хука, либо они хотят вызвать API или сделать любую другую операцию в нем. Но одно можно сказать наверняка, они должны возвращать Promise из этой функции, как того требует определение типа этого параметра. Поскольку он всегда будет возвращать Promise, поэтому мы ждем его, и если он разрешается, мы устанавливаем состояние isSubmitting в false.

В блоке catch прежде всего мы устанавливаем состояние isSubmitting на false, затем нам нужно проверить, не возникает ли какая-либо ошибка проверки, и если это произойдет, мы установим их в состояние. Поскольку мы вызываем две разные функции в блоке try, ошибка может быть разного типа: либо это ошибка проверки, выданная функцией validate, либо неизвестная ошибка, выданная функцией submitHandler.
Чтобы отличить ошибку проверки от неизвестной ошибки, мы используем функцию защиты типа под названием isValidationError. Эта функция использует функцию предиката типа TypeScript. Мы проверяем наличие свойства inner в объекте ошибки и возвращаем предикат типа err is ValidationError. Он будет работать как во время компиляции, так и во время выполнения. Во время компиляции он сузит тип объекта err с any до ValidationError, а во время выполнения проверит наличие свойства inner в объекте err, и наш условный оператор будет работать на основе этого. Здесь важно отметить одну вещь: мы не передаем touchedFields во втором параметре функции serializeYupErrors, потому что мы хотим установить ошибки для каждого поля при отправке формы, а не только для затронутых полей.
После проверки мы просто установим для всех полей значение true в состоянии touched, потому что, если мы этого не сделаем, то, как только произойдет событие размытия в любом поле, будет запущена наша функция проверки, и поскольку в событии размытия мы устанавливаем только ошибки затронутых полей, ошибка нетронутые поля исчезнут.

Вспомогательные функции:

В конце концов, мы сделали несколько вспомогательных функций, которые полезны, когда потребитель этого хука хочет явно установить значения или ошибки.

Это завершает эту длинную статью. Я надеюсь, что я освещаю каждый пункт ясно. Не стесняйтесь задавать любые вопросы, если вам что-то трудно понять.

Вы можете найти полный код с примером использования в этой песочнице кодов.

Последние мысли:

Я сделал этот хук для одного из своих проектов, в котором я решил использовать сторонние библиотеки как можно меньше. Этот хук создан для некоторых простых случаев использования. Он поддерживает только линейную форму данных и не поддерживает структуру вложенных объектов или массив объектов. Не стесняйтесь расширять его функциональность в соответствии с вашими потребностями. Но я бы рекомендовал делать это только в том случае, если вы делаете это с целью обучения, в противном случае лучше использовать популярные библиотеки, такие как React-Hook-Form или Formik, если вы используете случаи более сложные.