Создание собственной библиотеки проверки React: основы (часть 1)
Опубликовано: 2022-03-10Я всегда думал, что библиотеки проверки формы — это круто. Я знаю, это нишевый интерес, но мы так часто их используем! По крайней мере, в моей работе — большая часть того, что я делаю, — это создание более или менее сложных форм с правилами проверки, которые зависят от предыдущих вариантов и путей. Понимание того, как будет работать библиотека проверки формы, имеет первостепенное значение.
В прошлом году я написал одну такую библиотеку проверки формы. Я назвал его «Калибровка», и вы можете прочитать вводную запись в блоге здесь. Это хорошая библиотека, которая предлагает большую гибкость и использует несколько иной подход, чем другие на рынке. Однако существует множество других замечательных библиотек — моя просто хорошо подошла для наших требований.
Сегодня я собираюсь показать вам, как написать собственную библиотеку проверки для React. Мы рассмотрим процесс шаг за шагом, и вы найдете примеры CodeSandbox по мере продвижения. К концу этой статьи вы будете знать, как написать свою собственную библиотеку проверки, или, по крайней мере, будете глубже понимать, как другие библиотеки реализуют «магию проверки».
- Часть 1: Основы
- Часть 2: Особенности
- Часть 3: Опыт
Шаг 1. Разработка API
Первым шагом при создании любой библиотеки является проектирование того, как она будет использоваться. Это закладывает основу для большой части предстоящей работы, и, на мой взгляд, это самое важное решение, которое вы собираетесь принять в своей библиотеке.
Важно создать API, который будет «простым в использовании» и в то же время достаточно гибким, чтобы можно было вносить улучшения и расширенные варианты использования в будущем. Мы постараемся поразить обе эти цели.
Мы собираемся создать собственный хук, который будет принимать один объект конфигурации. Это позволит передавать будущие параметры без внесения критических изменений.
Примечание о крючках
Хуки — довольно новый способ написания React. Если вы писали React в прошлом, вы можете не знать некоторые из этих концепций. В этом случае, пожалуйста, ознакомьтесь с официальной документацией. Она невероятно хорошо написана и знакомит вас с основами, которые вам необходимо знать.
Сейчас мы назовем наш пользовательский хук useValidation
. Его использование может выглядеть примерно так:
const config = { fields: { username: { isRequired: { message: 'Please fill out a username' }, }, password: { isRequired: { message: 'Please fill out a password' }, isMinLength: { value: 6, message: 'Please make it more secure' } } }, onSubmit: e => { /* handle submit */ } }; const { getFieldProps, getFormProps, errors } = useValidation(config);
Объект config
принимает реквизит fields
, который устанавливает правила проверки для каждого поля. Кроме того, он принимает обратный вызов при отправке формы.
Объект fields
содержит ключ для каждого поля, которое мы хотим проверить. Каждое поле имеет свою собственную конфигурацию, где каждый ключ — это имя валидатора, а каждое значение — это свойство конфигурации для этого валидатора. Другой способ написать то же самое:
{ fields: { fieldName: { oneValidator: { validatorRule: 'validator value' }, anotherValidator: { errorMessage: 'something is not as it should' } } } }
Наш хук useValidation
вернет объект с несколькими свойствами — getFieldProps
, getFormProps
и errors
. Две первые функции — это то, что Кент С. Доддс называет «получателями свойств» (см. здесь отличную статью о них), и они используются для получения соответствующих свойств для заданного поля формы или тега формы. Свойство errors
— это объект с любыми сообщениями об ошибках, вводимыми для каждого поля.
Это использование будет выглядеть так:
const config = { ... }; // like above const LoginForm = props => { const { getFieldProps, getFormProps, errors } = useValidation(config); return ( <form {...getFormProps()}> <label> Username<br/> <input {...getFieldProps('username')} /> {errors.username && <div className="error">{errors.username}</div>} </label> <label> Password<br/> <input {...getFieldProps('password')} /> {errors.password && <div className="error">{errors.password}</div>} </label> <button type="submit">Submit my form</button> </form> ); };
Хорошо! Итак, мы прибили API.
- См. демонстрацию CodeSandbox
Обратите внимание, что мы также создали фиктивную реализацию хука useValidation
. На данный момент он просто возвращает объект с объектами и функциями, которые нам нужны, поэтому мы не нарушаем нашу примерную реализацию.
Сохранение состояния формы
Первое, что нам нужно сделать, это сохранить все состояние формы в нашем пользовательском хуке. Нам нужно помнить значения каждого поля, любые сообщения об ошибках и то, была ли форма отправлена. Мы будем использовать для этого хук useReducer
, так как он обеспечивает максимальную гибкость (и меньше шаблонов). Если вы когда-либо использовали Redux, вы увидите некоторые знакомые концепции, а если нет, мы объясним по мере продвижения! Мы начнем с написания редьюсера, который будет передан в хук useReducer
:
const initialState = { values: {}, errors: {}, submitted: false, }; function validationReducer(state, action) { switch(action.type) { case 'change': const values = { ...state.values, ...action.payload }; return { ...state, values, }; case 'submit': return { ...state, submitted: true }; default: throw new Error('Unknown action type'); } }
Что такое Редуктор?
Редуктор — это функция, которая принимает объект значений и «действие» и возвращает расширенную версию объекта значений.
Действия — это простые объекты JavaScript со свойством type
. Мы используем оператор switch
для обработки каждого возможного типа действия.
«Объект значений» часто называют состоянием , и в нашем случае это состояние нашей логики проверки.
Наше состояние состоит из трех частей данных — values
(текущие значения полей нашей формы), errors
(текущий набор сообщений об ошибках) и флага isSubmitted
, указывающего, была ли наша форма отправлена хотя бы один раз.
Чтобы сохранить состояние формы, нам нужно реализовать несколько частей хука useValidation
. Когда мы вызываем наш метод getFieldProps
, нам нужно вернуть объект со значением этого поля, обработчик изменений, когда оно изменяется, и свойство имени, чтобы отслеживать, какое поле есть какое.
function validationReducer(state, action) { // Like above } const initialState = { /* like above */ }; const useValidation = config => { const [state, dispatch] = useReducer(validationReducer, initialState); return { errors: state.errors, getFormProps: e => {}, getFieldProps: fieldName => ({ onChange: e => { if (!config.fields[fieldName]) { return; } dispatch({ type: 'change', payload: { [fieldName]: e.target.value } }); }, name: fieldName, value: state.values[fieldName], }), }; };
Метод getFieldProps
теперь возвращает свойства, необходимые для каждого поля. Когда событие изменения запускается, мы гарантируем, что поле находится в нашей конфигурации проверки, а затем сообщаем нашему редюсеру, что произошло действие change
. Редуктор будет обрабатывать изменения состояния проверки.
- См. демонстрацию CodeSandbox
Проверка нашей формы
Наша библиотека проверки формы выглядит хорошо, но мало что делает с точки зрения проверки значений формы! Давайте исправим это.
Мы собираемся проверять все поля при каждом событии изменения. Это может показаться не очень эффективным, но в реальных приложениях, с которыми я сталкивался, это не проблема.
Обратите внимание: мы не говорим, что вы должны показывать каждую ошибку при каждом изменении. Мы вернемся к тому, как отображать ошибки только при отправке или переходе от поля, позже в этой статье.
Как выбрать функции валидатора
Когда дело доходит до валидаторов, существует множество библиотек, которые реализуют все методы проверки, которые вам когда-либо понадобятся. Вы также можете написать свой собственный, если хотите. Это веселое упражнение!
Для этого проекта мы будем использовать набор валидаторов, который я написал некоторое время назад — calidators
. Эти валидаторы имеют следующий API:
function isRequired(config) { return function(value) { if (value === '') { return config.message; } else { return null; } }; } // or the same, but terser const isRequired = config => value => value === '' ? config.message : null;
Другими словами, каждый валидатор принимает объект конфигурации и возвращает полностью настроенный валидатор. Когда эта функция вызывается со значением, она возвращает свойство message
, если значение недействительно, или null
, если оно допустимо. Вы можете посмотреть, как реализованы некоторые из этих валидаторов, просмотрев исходный код.
Чтобы получить доступ к этим валидаторам, установите пакет calidators
с помощью npm install calidators
.
Проверка одного поля
Помните конфигурацию, которую мы передаем в наш объект useValidation
? Это выглядит так:
{ fields: { username: { isRequired: { message: 'Please fill out a username' }, }, password: { isRequired: { message: 'Please fill out a password' }, isMinLength: { value: 6, message: 'Please make it more secure' } } }, // more stuff }
Чтобы упростить нашу реализацию, давайте предположим, что у нас есть только одно поле для проверки. Мы пройдемся по каждому ключу объекта конфигурации поля и запустим валидаторы один за другим, пока не найдем ошибку или не закончим проверку.
import * as validators from 'calidators'; function validateField(fieldValue = '', fieldConfig) { for (let validatorName in fieldConfig) { const validatorConfig = fieldConfig[validatorName]; const validator = validators[validatorName]; const configuredValidator = validator(validatorConfig); const errorMessage = configuredValidator(fieldValue); if (errorMessage) { return errorMessage; } } return null; }
Здесь мы написали функцию validateField
, которая принимает значение для проверки и настройки валидатора для этого поля. Мы перебираем все валидаторы, передаем им конфигурацию этого валидатора и запускаем его. Если мы получаем сообщение об ошибке, мы пропускаем остальные валидаторы и возвращаемся. Если нет, пробуем следующий валидатор.
Примечание. В API-интерфейсах валидатора
Если вы выберете разные валидаторы с разными API (например, очень популярный validator.js
), эта часть вашего кода может выглядеть немного иначе. Однако для краткости мы оставляем эту часть в качестве упражнения читателю.
Примечание: в цикле for…in
Никогда раньше не использовали циклы for...in
? Это нормально, это был мой первый раз тоже! По сути, он перебирает ключи в объекте. Подробнее о них можно прочитать на MDN.
Подтвердите все поля
Теперь, когда мы проверили одно поле, мы должны без особых проблем проверить все поля.
function validateField(fieldValue = '', fieldConfig) { // as before } function validateFields(fieldValues, fieldConfigs) { const errors = {}; for (let fieldName in fieldConfigs) { const fieldConfig = fieldConfigs[fieldName]; const fieldValue = fieldValues[fieldName]; errors[fieldName] = validateField(fieldValue, fieldConfig); } return errors; }
Мы написали функцию validateFields
, которая принимает все значения полей и всю конфигурацию поля. Мы перебираем каждое имя поля в конфигурации и проверяем это поле с его объектом конфигурации и значением.
Далее: Расскажите нашему редуктору
Итак, теперь у нас есть эта функция, которая проверяет все наши вещи. Давайте добавим его в остальную часть нашего кода!
Во-первых, мы собираемся добавить обработчик действия validate
в наш validationReducer
.
function validationReducer(state, action) { switch (action.type) { case 'change': // as before case 'submit': // as before case 'validate': return { ...state, errors: action.payload }; default: throw new Error('Unknown action type'); } }
Всякий раз, когда мы запускаем действие validate
, мы заменяем ошибки в нашем состоянии тем, что было передано вместе с действием.
Далее мы собираемся запустить нашу логику проверки из хука useEffect
:
const useValidation = config => { const [state, dispatch] = useReducer(validationReducer, initialState); useEffect(() => { const errors = validateFields(state.fields, config.fields); dispatch({ type: 'validate', payload: errors }); }, [state.fields, config.fields]); return { // as before }; };
Этот хук useEffect
запускается всякий раз, когда изменяются поля state.fields
или config.fields
, а также при первом монтировании.
Остерегайтесь ошибки
В приведенном выше коде есть очень тонкая ошибка. Мы указали, что наш хук useEffect
должен перезапускаться только при изменении state.fields
или config.fields
. Оказывается, «изменение» не обязательно означает изменение ценности! useEffect
использует Object.is
для обеспечения равенства между объектами, что, в свою очередь, использует равенство ссылок. То есть — если вы передадите новый объект с тем же содержимым, он не будет таким же (поскольку сам объект новый).
state.fields
возвращаются из useReducer
, что гарантирует нам это ссылочное равенство, но наша config
указана встроенной в наш компонент функции. Это означает, что объект воссоздается при каждом рендеринге, что, в свою очередь, вызывает указанный выше useEffect
!
Чтобы решить эту проблему, нам нужно использовать библиотеку use-deep-compare-effect
от Kent C. Dodds. Вы устанавливаете его с помощью npm install use-deep-compare-effect
и вместо этого заменяете свой вызов useEffect
. Это гарантирует, что мы выполняем глубокую проверку равенства вместо проверки равенства ссылок.
Теперь ваш код будет выглядеть так:
import useDeepCompareEffect from 'use-deep-compare-effect'; const useValidation = config => { const [state, dispatch] = useReducer(validationReducer, initialState); useDeepCompareEffect(() => { const errors = validateFields(state.fields, config.fields); dispatch({ type: 'validate', payload: errors }); }, [state.fields, config.fields]); return { // as before }; };
Примечание об использованииЭффект
Оказывается, useEffect
— довольно интересная функция. Дэн Абрамов написал очень хорошую, длинную статью о тонкостях useEffect
, если вам интересно узнать все об этом хуке.
Теперь все начинает выглядеть как библиотека проверки!
- См. демонстрацию CodeSandbox
Обработка отправки формы
Последняя часть нашей базовой библиотеки проверки формы обрабатывает то, что происходит, когда мы отправляем форму. Сейчас перезагружает страницу и ничего не происходит. Это не оптимально. Мы хотим предотвратить поведение браузера по умолчанию, когда дело доходит до форм, и вместо этого обрабатывать его самостоятельно. Мы размещаем эту логику внутри функции получения реквизита getFormProps
:
const useValidation = config => { const [state, dispatch] = useReducer(validationReducer, initialState); // as before return { getFormProps: () => ({ onSubmit: e => { e.preventDefault(); dispatch({ type: 'submit' }); if (config.onSubmit) { config.onSubmit(state); } }, }), // as before }; };
Мы меняем нашу функцию getFormProps
, чтобы она возвращала функцию onSubmit
, которая срабатывает всякий раз, когда инициируется событие submit
DOM. Мы предотвращаем поведение браузера по умолчанию, отправляем действие, чтобы сообщить нашему редьюсеру, что мы отправили его, и вызываем предоставленный обратный вызов onSubmit
со всем состоянием — если он предоставлен.
Резюме
Были там! Мы создали простую, удобную и довольно крутую библиотеку проверки. Однако предстоит еще много работы, прежде чем мы сможем доминировать в сети.
- Часть 1: Основы
- Часть 2: Особенности
- Часть 3: Опыт