React Hook Form: создание сложных форм для начинающих

от автора

Привет, Хабр!

Сегодня рассмотрим важную тему для всех, кто занимается созданием сложных и многошаговых форм в 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» позволяют юзеру добавлять и удалять поля соответственно.

Оптимизация производительности форм

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

  1. Избегайте ненужных рендеров: используйте React.memo и useMemo, чтобы избежать повторных рендеров компонентов, которые не зависят от состояния формы.

  2. Контролируемые и неконтролируемые компоненты: React Hook Form использует неконтролируемые компоненты по дефолту, что снижает количество рендеров. Если нужно использовать контролируемые компоненты, убеждаемся, что мы оптимизируем их правильно.

  3. Ленивая загрузка компонентов: загружаем компоненты формы по мере необходимости, а не все сразу.

Пример:

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: в каталоге можно посмотреть список всех программ, а в календаре — записаться на открытые уроки.


ссылка на оригинал статьи https://habr.com/ru/articles/829326/