Создание собственной библиотеки валидации для React: основы (часть 1)

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

Содержание

Шаг 1: Разработка API

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

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

Примечание о хуках

Хуки — новый функционал, который появился в 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:

{
  fields: {
    fieldName: {
      oneValidator: { validatorRule: 'validator value' },
      anotherValidator: { errorMessage: 'something is not as it should' }
    }
  }
}

Хук

useValidation
возвращает объект с несколькими свойствами: 
getFieldProps
getFormProps
и 
errors
. Две первые функции — это геттеры свойств. Они позволяют применить соответствующие свойства для данного поля формы или тега формы. Свойство
errors
представляет собой объект с сообщениями об ошибках ввода пользователя. Пример использования:

const config = { ... }; // как и выше
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.

Посмотреть демо

Обратите внимание, что мы также создали имитацию реализации хука

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) {
  // Как и выше
}

const initialState = { /* Как и выше */ };

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, мы проверяем, чтобы поле находилось в конфигурации валидации. После этого сообщаем редюсеру, что произошло событие
change
. Он обрабатывает изменения в состоянии валидации.

Посмотреть демо

Валидация формы

Мы собираемся проверить все поля при каждом событии change. Я не говорю, что нужно отображать каждую ошибку при каждом изменении. Мы научимся выводить ошибки только при отправке данных или выходе из поля.

Как выбрать функции валидатора

В этом проекте мы будем использовать набор валидаторов calidators, которые я написал раньше. Эти валидаторы имеют следующий API:

function isRequired(config) {
  return function(value) {
    if (value === '') {
      return config.message;
    } else {
      return null;
    }
  };
}

// или то же самое, но более кратко

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' }
    }
  },
  // остальной код
}

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

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
, которая принимает значение для проверки и настройки валидатора для этого поля. Мы перебираем все валидаторы, передаем им конфигурацию для этого валидатора и запускаем его. Если мы получим сообщение об ошибке, то пропустим остальные валидаторы и вернемся. Если нет, запускаем следующий валидатор.

Примечание относительно цикла  for…in

Никогда раньше не использовали циклы for…in?  По сути, он перебирает ключи в объекте. Вы можете узнать больше на MDN.

Валидация всех полей

Теперь, когда проверили одно поле, мы сможем проверить все поля.

function validateField(fieldValue = '', fieldConfig) {
  // как и раньше
}

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':
      // как и раньше
    case 'submit':
      // как и раньше
    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 {
    // как и раньше
  };
};

Хук 

useEffect
запускается каждый раз, когда изменяется 
state.fields
или 
config.fields
.

Остерегайтесь ошибок

В приведенном выше коде есть ошибка. Мы указали, что хук 

useEffect
должен повторяться только при изменении 
state.fields
или 
config.fields

Оказывается, это касается не только изменения значения. 

useEffect
использует 
Object.is
для обеспечения равенства между объектами. То есть — если вы передадите новый объект с тем же содержимым, он не будет таким же. Поскольку сам объект является новым.

state.fields
возвращается из 
useReducer
, что гарантирует нам равенство ссылок. Но
config
встроена в функцию. Это означает, что объект воссоздается при каждом рендеринге. Что, в свою очередь, запускает 
useEffect
!

Чтобы решить эту проблему, используем библиотеку use-deep-compare-effect. Установите ее с помощью

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
  };
};

Посмотреть демо

Обработка отправки данных формы

Последняя часть библиотеки валидации формы обрабатывает отправку данных формы. Нужно изменить поведение браузера относительно форм. Мы помещаем эту логику в функцию

getFormProps
:

const useValidation = config => {
  const [state, dispatch] = useReducer(validationReducer, initialState);
  // как и раньше
  return {
    getFormProps: () => ({
      onSubmit: e => {
        e.preventDefault();
        dispatch({ type: 'submit' });
        if (config.onSubmit) {
          config.onSubmit(state);
        }
      },
    }),
    // как и раньше
  };
};

Теперь

getFormProps
возвращает функцию
onSubmit
. Она запускается каждый раз при событии DOM
submit
. Мы предотвращаем поведение браузера, используемое по умолчанию. А затем сообщаем редюсеру, что мы отправили данные. После этого осуществляем обратный вызов
onSubmit
со всем состоянием.

Заключение

Мы создали простую библиотеку валидации. Но нам еще предстоит проделать огромную работу по ее усовершенствованию.

Данная публикация представляет собой перевод статьи «Creating Your Own React Validation Library: The Basics (Part 1)» , подготовленной дружной командой проекта Интернет-технологии.ру

Меню