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

Уже существует множество библиотек для форм, чтобы упростить этот процесс. Двумя из самых больших, на которые я смотрел, прежде чем я решил реализовать свои собственные хуки, были redux-form и formik. Хотя оба казались хорошими альтернативами, я просто погрузил пальцы ног в крючки и хотел изучить все тонкости крючков. В конце концов, почему бы не пойти родным, если можно?

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

Все наши формы имеют от 2 до 10 полей, и после завершения данные просто передаются в наш бэкэнд через вызов api внутри ловушки. Образец формы показан ниже. Как видите, когда пользователь переходит к следующему полю, ничего не вводя в обязательное поле, граница ввода становится красной и появляется сообщение об ошибке. Все это происходит мгновенно, как вы позже увидите из мгновенной проверки после onBlur, onChange и onSubmit.

Если пользователь вводит что-то неверное, мы снова видим те же ошибки:

Самое замечательное в ловушке - это то, что она проверяет наличие ошибок ДО того, как даже задумываться о вызове api! Если пользователь отправляет запрос и ошибок НЕТ, то, наконец, запускается вызов API. Проверка на стороне клиента перед передачей данных формы на сервер - это еще один способ помочь отловить ошибки и передовой опыт.

ВАЛИДАЦИОННЫЙ КРЮЧОК

Итак, теперь, когда я рассказал о том, что делает крючок, и показал несколько примеров того, почему он хорош, вот крючок (далее мы рассмотрим его):

импортировать {useState, useEffect} из «реакции»;

// hook to validate all forms
// accepts initial state for values, validation funtion, 
// and method to run when there are no errors
const useFormValidation = (initialState, validate, runOnSubmit) => {
    const [values, setValues] = useState(initialState);
    const [errors, setErrors] = useState({});
    // touched is an array of all fields that have 
    // been touched by the user
    const [touched, setTouched] = useState([]);
    const [isSubmitting, setSubmitting] = useState(false);

    // use effect runs on intial component load 
    // and every time the errors object changes
    useEffect(() => {
        // only call form submission if submit was hit
        if (isSubmitting) {
            const noErrors = Object.keys(errors).length === 0;
            // only call form submission if there are no errors
            if (noErrors) {
                // clear out touched upon submission
                setTouched([]);
                // run form submission when no errors
                runOnSubmit();
                setSubmitting(false);
            } else {
                setSubmitting(false);
            }
        }
        // disabling the eslint error
        // reasons behind my opinions to not memoize functions
        // https://github.com/facebook/create-react-app/issues/6880
        // isSubmitting also would trigger extra render if added
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [errors]);

    // need to rerun after there is a changed to touched or values
    // this checks to see if there are any errors 
    // that should be highlighted
    useEffect(() => {
        const validationErrors = validate(values);
        const touchedErrors = Object.keys(validationErrors)
            .filter(key => touched.includes(key))
            .reduce((acc, key) => {
                if (!acc[key]) {
                    acc[key] = validationErrors[key]
                }
                return acc
            }, {})
        setErrors(touchedErrors);
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [touched, values]);

    // handle change is run every time an input changes
    const handleChange = (event) => {
        setValues({
            ...values,
            [event.target.name]: event.target.value
        });
    }

    // handle blur is run when a form is tapped or 
    // tabbed into/ 'touched'
    const handleBlur = (event) => {
        // if touched hasn't been touched before add it to the array
        if (!touched.includes(event.target.name)) {
            setTouched([
                ...touched,
                event.target.name
            ])
        }
    }

    // function for form submission
    const handleSubmit = (event) => {
        event.preventDefault();
        const validationErrors = validate(values);
        setErrors(validationErrors);
        setSubmitting(true);
    }

    // return values from hook to be used in react form component
    return {
        handleSubmit,
        handleChange,
        handleChangeChecked,
        handleBlur,
        values,
        errors,
        isSubmitting
    };
}

export default useFormValidation;

По сути, useEffect похож на componentDidMount и componentDidUpdate. Он запускается, когда компонент подключается к экрану, а затем запускается снова при изменении любой из зависимостей (переменных внутри массива).

У меня работают 2 эффекта использования, первый из которых используется для отправки - он проверяет, можно ли отправить форму / если есть ошибки, он сразу же отклоняет отправку. Второй проверяет форму. Он проверяет, касаются ли поля. Как только к нему прикасаются, он добавляется в массив. Кроме того, он проверяет форму на соответствие функции проверки, которая передается в ловушку. В конечном итоге, если произошла ошибка и было затронуто это поле формы, то эта конкретная ошибка добавляется к объекту ошибок.

ФАЙЛ ВАЛИДАЦИИ

Здесь вы устанавливаете то, что хотите проверить. Я привожу пару примеров, но вы можете проверить любое поле, используя такие вещи, как RegEx или javascript. Для следующих двух разделов я попытался выбрать несколько различных примеров, используемых в нашем веб-приложении. Этот файл принимает значения каждый раз, когда происходит изменение компонента или его отправка.

// validate function
// pass in the values object
const validateExample = (values) => {
  // initialize errors object
  let errors = {};
  // if errors exist for each field then 
  // add them to the errors object

  // First Name Errors
  // This is just saying if name doesn't exist
  if (!values.name) {
    errors.name = "Name is required";
  }

  // Email Error
  // make sure email is correct format using RegEx
  // email is not required so only check if 
  // email fits if one is provided
  if (values.email && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z] .    {2,}$/i.test(values.email)) {
    errors.email = "Please enter a valid email address";
  }

  const phoneno = /^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/
  // Phone Number Errors
  if (!values.phoneNumber) {
    errors.phoneNumber = "Phone number is required";
  } else if (!values.phone.match(phoneno)) {
    errors.phoneNumber = "Please enter a phone number with 10  digits.  1 not necessary"
  }

  // Date of Birth Errors
  // check to make sure all fields are provided
  // if they are make sure everything else is accurate
  if (!values.dateOfBirthYear) {
    errors.dateOfBirthYear = "Please enter your birth year";
  }
  else if (values.dateOfBirthYear <= 1900 || values.dateOfBirthYear > 2019) {
    errors.dateOfBirthYear = "Please enter a valid year between 1900-2019";
  }

  return errors;
}

export default validateExample;

КОМПОНЕНТ

Вот собственно реагирующий компонент. Здесь вы можете увидеть ловушку, вызываемую с помощью функции проверки сверху, и все выполняемые функции из ловушки. Обратите внимание, что значения onBlur, onChange и все используются с каждым вводом, а onSubmit используется после того, как форма будет заполнена и отправлена.

Над каждым вводом вы можете видеть, что отображается ошибка, если она существует, и использование ClassNames (потрясающий пакет npm). ClassNames позволяет выбирать классы на основе условной логики, поэтому при возникновении ошибки мы делаем контейнер ввода красным.

import React from 'react';
import styles from './componentExample.module.scss';
import useFormValidation from "./useFormValidation";
import validateExample from "./validateExample";

// great package for conditional classnames
// need for dealing with error vs regular state
// https://www.npmjs.com/package/classnames
import classNames from 'classnames/bind';

const componentExample = () => {

    const INITIAL_STATE = {
        name: "",
        email: "",
        phoneNumber: "",
        dateOfBirthYear: ""
    }

    const submitFunction = async () => {
        const { name, email, phoneNumber, dateOfBirthYear } = values
        // here is where you would call something like an api
        // call your backend and post the form data

    }

    // call the hook
    const {
        handleSubmit,
        handleChange,
        handleBlur,
        values,
        errors,
        isSubmitting
    } = useFormValidation(INITIAL_STATE, validateExample,           submitFunction);

    const { container, contentContainer, input, errorText, errorInput } = styles;
    return (
        <div className={container}>
            <form onSubmit={handleSubmit}>
                <div className={contentContainer}>
                    <div>
                        Form is below
                    </div>
                    {/* if there is an error render error text */}
                    {errors.name && <p className={errorText}>*{errors.name}</p>}
                    {/* if there is an error make the input field use errorInput styling */}
                    <input
                        className={classNames(errors.name && errorInput, input)}
                        name="name"
                        placeholder="Name"
                        type="text"
                        onChange={handleChange}
                        value={values.name}
                        onBlur={handleBlur}
                    />
                    {errors.email && <p className={errorText}>*{errors.email}</p>}
                    <input
                        className={classNames(errors.email && errorInput, input)}
                        name="email"
                        placeholder="Email"
                        onChange={handleChange}
                        value={values.email}
                        onBlur={handleBlur}
                    />
                    {errors.phoneNumber && <p className={errorText}>*{errors.phoneNumber}</p>}
                    <input
                        className={classNames(errors.phoneNumber && errorInput, input)}
                        name="phoneNumber"
                        placeholder="Phone Number"
                        type="number"
                        onChange={handleChange}
                        value={values.phoneNumber}
                        onBlur={handleBlur}
                    />
                    {errors.dateOfBirthYear && <p className={errorText}>*{errors.dateOfBirthYear}</p>}
                    <input
                        className={classNames(errors.dateOfBirthYear && errorInput, input)}
                        name="dateOfBirthYear"
                        placeholder="YYYY"
                        type="number"
                        onChange={handleChange}
                        value={values.dateOfBirthYear}
                        onBlur={handleBlur}
                    />
                    <button
                        disabled={isSubmitting}
                        type="submit">
                        Submit
                        </button>
                </div>
            </form>
        </div>
    )
}

export default componentExample;

CSS МОДУЛЬ

Наконец, вот CSS для ошибок. Я использую модули SASS и CSS, потому что они потрясающие.

.input {
    width: 300px;
    height: px;
    border-radius: 5px;
    border: solid 2px grey;
    margin-bottom: 16px;
}

.errorInput {
    border: solid 2px red;
}

.errorText {
    font-family: Roboto;
    font-size: 14px;
    color: red;
    width: 300px;
}

Спасибо, что прочитали мою первую статью, и вот исходный код:



Надеюсь, это поможет всем избавиться от головной боли, связанной с формами, и узнать больше о том, как можно использовать хуки для упрощения повседневного кода :)