Привет, Хабр!
Сегодня рассмотрим важную тему для всех, кто занимается созданием сложных и многошаговых форм в React. Мы все знаем, как это бывает: бесконечные рендеры, тонны кода для валидации и управления состоянием, а также бесконечная борьба за оптимизацию производительности. Но никто уже давно не отчаивается, ведь существует мощное и гибкое решение — React Hook Form.
React Hook Form — это библиотека, которая использует концепцию неконтролируемых компонентов, чтобы минимизировать количество повторных рендеров и повысить производительность приложения.
Данная статья полезна для новичков, которые только начинают работать со сложными формами в React.
Создание сложных форм
Разделение формы на компоненты
Разделение формы на компоненты — это основа, которая позволяет создавать многошаговые формы. Каждый шаг формы может быть представлен отдельным компонентом:
// Step 1: PersonalInfo.js
import React from 'react';
import { useForm } from 'react-hook-form';
import { useHistory } from 'react-router-dom';
const PersonalInfo = ({ onSubmit }) => {
  const { register, handleSubmit } = useForm();
  const history = useHistory();
  const handleNext = (data) => {
    onSubmit(data);
    history.push('/employment');
  };
  return (
    <form onSubmit={handleSubmit(handleNext)}>
      <div>
        <label htmlFor="firstName">First Name</label>
        <input id="firstName" {...register('firstName', { required: true })} />
      </div>
      <div>
        <label htmlFor="lastName">Last Name</label>
        <input id="lastName" {...register('lastName', { required: true })} />
      </div>
      <button type="submit">Next</button>
    </form>
  );
};
export default PersonalInfo;Контекст формы для управления состоянием данных
Контекст формы позволяетпередавать состояние формы между различными компонентами. Это можно сделать с помощью FormProvider и useFormContext:
// FormContext.js
import React, { createContext, useContext, useState } from 'react';
const FormContext = createContext();
export const useFormData = () => useContext(FormContext);
export const FormProvider = ({ children }) => {
  const [formData, setFormData] = useState({});
  const updateFormData = (data) => {
    setFormData((prev) => ({ ...prev, ...data }));
  };
  return (
    <FormContext.Provider value={{ formData, updateFormData }}>
      {children}
    </FormContext.Provider>
  );
};Пример создания шагов формы и их связка через React Router
Через React Router можно управлять навигацией между различными шагами формы:
// App.js
import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { FormProvider } from './FormContext';
import PersonalInfo from './PersonalInfo';
import Employment from './Employment';
import Review from './Review';
const App = () => (
  <Router>
    <FormProvider>
      <Switch>
        <Route path="/" exact component={PersonalInfo} />
        <Route path="/employment" component={Employment} />
        <Route path="/review" component={Review} />
      </Switch>
    </FormProvider>
  </Router>
);
export default App;// Step 2: Employment.js
import React from 'react';
import { useForm } from 'react-hook-form';
import { useHistory } from 'react-router-dom';
import { useFormData } from './FormContext';
const Employment = () => {
  const { register, handleSubmit } = useForm();
  const history = useHistory();
  const { updateFormData } = useFormData();
  const handleNext = (data) => {
    updateFormData(data);
    history.push('/review');
  };
  return (
    <form onSubmit={handleSubmit(handleNext)}>
      <div>
        <label htmlFor="company">Company</label>
        <input id="company" {...register('company', { required: true })} />
      </div>
      <div>
        <label htmlFor="position">Position</label>
        <input id="position" {...register('position', { required: true })} />
      </div>
      <button type="submit">Next</button>
    </form>
  );
};
export default Employment;Подключение и управление состоянием формы с помощью хуков
Хуки useForm, useFormContext и другие позволяет управлять состоянием формы и валидировать данные:
// Step 3: Review.js
import React from 'react';
import { useFormData } from './FormContext';
const Review = () => {
  const { formData } = useFormData();
  const handleSubmit = () => {
    console.log('Form submitted:', formData);
    // Add submission logic here
  };
  return (
    <div>
      <h2>Review Your Information</h2>
      <pre>{JSON.stringify(formData, null, 2)}</pre>
      <button onClick={handleSubmit}>Submit</button>
    </div>
  );
};
export default Review;
Валидация и обработка ошибок
Рассмотрим настройку встроенной валидации.
Можно настривать валидацию с помощью атрибутов required, minLength, maxLength, pattern, и validate.
Пример:
import React from 'react';
import { useForm } from 'react-hook-form';
const SimpleForm = () => {
  const { register, handleSubmit, formState: { errors } } = useForm();
  const onSubmit = (data) => {
    console.log(data);
  };
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="name">Name</label>
        <input
          id="name"
          {...register('name', { required: 'Name is required' })}
        />
        {errors.name && <p>{errors.name.message}</p>}
      </div>
      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          {...register('email', {
            required: 'Email is required',
            pattern: {
              value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i,
              message: 'Invalid email address',
            },
          })}
        />
        {errors.email && <p>{errors.email.message}</p>}
      </div>
      <button type="submit">Submit</button>
    </form>
  );
};
export default SimpleForm;Интеграция с библиотеками для схемной валидации
Для более хардовой валидации можно использовать библиотеки Yup и Zod. Эти библиотеки позволяют создавать схемы валидации и интегрировать их с React Hook Form.
Пример с Yup:
import React from 'react';
import { useForm, Controller } from 'react-hook-form';
import * as yup from 'yup';
import { yupResolver } from '@hookform/resolvers/yup';
const schema = yup.object().shape({
  name: yup.string().required('Name is required'),
  email: yup.string().email('Invalid email').required('Email is required'),
});
const YupForm = () => {
  const { control, handleSubmit, formState: { errors } } = useForm({
    resolver: yupResolver(schema),
  });
  const onSubmit = (data) => {
    console.log(data);
  };
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="name">Name</label>
        <Controller
          name="name"
          control={control}
          render={({ field }) => <input id="name" {...field} />}
        />
        {errors.name && <p>{errors.name.message}</p>}
      </div>
      <div>
        <label htmlFor="email">Email</label>
        <Controller
          name="email"
          control={control}
          render={({ field }) => <input id="email" {...field} />}
        />
        {errors.email && <p>{errors.email.message}</p>}
      </div>
      <button type="submit">Submit</button>
    </form>
  );
};
export default YupForm;Обработка и отображение ошибок
Можно управлять отображением ошибок с помощью объекта errors, предоставляемого хукем useForm, пример:
import React from 'react';
import { useForm } from 'react-hook-form';
const ErrorHandlingForm = () => {
  const { register, handleSubmit, formState: { errors } } = useForm();
  const onSubmit = (data) => {
    console.log(data);
  };
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="username">Username</label>
        <input
          id="username"
          {...register('username', { required: 'Username is required' })}
        />
        {errors.username && <p>{errors.username.message}</p>}
      </div>
      <div>
        <label htmlFor="password">Password</label>
        <input
          id="password"
          type="password"
          {...register('password', {
            required: 'Password is required',
            minLength: { value: 8, message: 'Password must be at least 8 characters' },
          })}
        />
        {errors.password && <p>{errors.password.message}</p>}
      </div>
      <button type="submit">Submit</button>
    </form>
  );
};
export default ErrorHandlingForm;Примеры для различных типов валидации
Пример валидации формы регистрации:
import React from 'react';
import { useForm } from 'react-hook-form';
const RegistrationForm = () => {
  const { register, handleSubmit, formState: { errors } } = useForm();
  const onSubmit = (data) => {
    console.log('Registration Data:', data);
  };
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="firstName">First Name</label>
        <input
          id="firstName"
          {...register('firstName', { required: 'First name is required' })}
        />
        {errors.firstName && <p>{errors.firstName.message}</p>}
      </div>
      <div>
        <label htmlFor="lastName">Last Name</label>
        <input
          id="lastName"
          {...register('lastName', { required: 'Last name is required' })}
        />
        {errors.lastName && <p>{errors.lastName.message}</p>}
      </div>
      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          {...register('email', {
            required: 'Email is required',
            pattern: {
              value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i,
              message: 'Invalid email address',
            },
          })}
        />
        {errors.email && <p>{errors.email.message}</p>}
      </div>
      <div>
        <label htmlFor="password">Password</label>
        <input
          id="password"
          type="password"
          {...register('password', {
            required: 'Password is required',
            minLength: {
              value: 8,
              message: 'Password must be at least 8 characters',
            },
          })}
        />
        {errors.password && <p>{errors.password.message}</p>}
      </div>
      <button type="submit">Register</button>
    </form>
  );
};
export default RegistrationForm;Пример кастомной валидации пароля:
import React from 'react';
import { useForm } from 'react-hook-form';
const CustomValidationForm = () => {
  const { register, handleSubmit, formState: { errors } } = useForm();
  const validatePassword = (value) => {
    if (value.length < 8) {
      return 'Password must be at least 8 characters long';
    } else if (!/[A-Z]/.test(value)) {
      return 'Password must contain at least one uppercase letter';
    } else if (!/[0-9]/.test(value)) {
      return 'Password must contain at least one number';
    }
    return true;
  };
  const onSubmit = (data) => {
    console.log('Form Data:', data);
  };
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="password">Password</label>
        <input
          id="password"
          type="password"
          {...register('password', {
            required: 'Password is required',
            validate: validatePassword,
          })}
        />
        {errors.password && <p>{errors.password.message}</p>}
      </div>
      <button type="submit">Submit</button>
    </form>
  );
};
export default CustomValidationForm;Прочие фичи
useFieldArray для динамического добавления/удаления полей формы
useFieldArray — это мощный хук в React Hook Form, который позволяет динамически управлять массивом полей формы. Мастхев для создания форм, в которых пользователи могут добавлять или удалять элементы.
Создадим форму, в которой можно добавлять и удалять поля для ввода имен участников команды:
import React from 'react';
import { useForm, useFieldArray } from 'react-hook-form';
const TeamForm = () => {
  const { register, control, handleSubmit } = useForm({
    defaultValues: {
      members: [{ name: '' }],
    },
  });
  const { fields, append, remove } = useFieldArray({
    control,
    name: 'members',
  });
  const onSubmit = (data) => {
    console.log(data);
  };
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <h2>Team Members</h2>
      {fields.map((field, index) => (
        <div key={field.id}>
          <input
            {...register(`members.${index}.name`, { required: 'Name is required' })}
            defaultValue={field.name}
          />
          <button type="button" onClick={() => remove(index)}>
            Remove
          </button>
        </div>
      ))}
      <button type="button" onClick={() => append({ name: '' })}>
        Add Member
      </button>
      <button type="submit">Submit</button>
    </form>
  );
};
export default TeamForm;Используем useForm для управления состоянием формы и useFieldArray для динамического управления массивом полей. Кнопки "Add Member" и "Remove" позволяют юзеру добавлять и удалять поля соответственно.
Оптимизация производительности форм
Когда есть формы с большим количеством полей, важно учитывать производительность, чтобы предотвратить задержки. И здесь у меня есть несколько советов:
- Избегайте ненужных рендеров: используйте - React.memoи- useMemo, чтобы избежать повторных рендеров компонентов, которые не зависят от состояния формы.
- Контролируемые и неконтролируемые компоненты: React Hook Form использует неконтролируемые компоненты по дефолту, что снижает количество рендеров. Если нужно использовать контролируемые компоненты, убеждаемся, что мы оптимизируем их правильно. 
- Ленивая загрузка компонентов: загружаем компоненты формы по мере необходимости, а не все сразу. 
Пример:
import React, { memo } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { TextField, Button } from '@material-ui/core';
const LargeForm = () => {
  const { control, handleSubmit } = useForm();
  
  const onSubmit = (data) => {
    console.log(data);
  };
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {Array.from({ length: 100 }).map((_, index) => (
        <Controller
          key={index}
          name={`field${index}`}
          control={control}
          defaultValue=""
          render={({ field }) => <TextField {...field} label={`Field ${index + 1}`} variant="outlined" />}
        />
      ))}
      <Button type="submit" variant="contained" color="primary">
        Submit
      </Button>
    </form>
  );
};
export default memo(LargeForm);Интеграция с внешними компонентами UI
React Hook Form легко интегрируется с внешними библиотеками компонентов UI, такими как Material-UI и Ant Design.
Пример кода с Material-UI:
import React from 'react';
import { useForm, Controller } from 'react-hook-form';
import { TextField, Button } from '@material-ui/core';
const MaterialUIForm = () => {
  const { control, handleSubmit } = useForm();
  const onSubmit = (data) => {
    console.log(data);
  };
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        name="firstName"
        control={control}
        defaultValue=""
        render={({ field }) => (
          <TextField {...field} label="First Name" variant="outlined" fullWidth margin="normal" />
        )}
      />
      <Controller
        name="lastName"
        control={control}
        defaultValue=""
        render={({ field }) => (
          <TextField {...field} label="Last Name" variant="outlined" fullWidth margin="normal" />
        )}
      />
      <Button type="submit" variant="contained" color="primary">
        Submit
      </Button>
    </form>
  );
};
export default MaterialUIForm;С React Hook Form создание сложных форм в React становится значительно проще!
Все актуальные методы и инструменты программирования можно освоить на онлайн-курсах OTUS: в каталоге можно посмотреть список всех программ, а в календаре — записаться на открытые уроки.
 
          