Урок 31 из 49 · Месяц 4. React
Формы: react-hook-form, валидация yup и react-toastify
Содержание урока
- Форма на useState: в чём проблема
- Установка библиотек
- react-hook-form: основы
- register — регистрируем поле
- handleSubmit — отправка
- errors — ошибки
- Что ещё умеет useForm
- Валидация с yup
- Пишем схему
- Подключаем схему к форме
- React-toastify: уведомления
- Подключение
- Показ уведомлений
- Собираем полную форму регистрации
- Типичные ошибки
- Практика
- Итоги
Формы есть почти в каждом приложении: регистрация, вход, оформление заказа, отзыв. В этом уроке посмотрим, как делать форму на чистом React и почему это быстро становится неудобно, затем перейдём на библиотеку react-hook-form, опишем правила проверки полей через yup и научимся показывать красивые всплывающие уведомления с помощью react-toastify.
Форма на useState: в чём проблема
Вспомним управляемые поля из урока useState и useEffect и сделаем форму входа с проверкой:
import { useState } from 'react';
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState({});
function handleSubmit(e) {
e.preventDefault();
const newErrors = {};
if (!email) newErrors.email = 'Введите email';
else if (!email.includes('@')) newErrors.email = 'Некорректный email';
if (password.length < 8) newErrors.password = 'Минимум 8 символов';
setErrors(newErrors);
if (Object.keys(newErrors).length === 0) {
console.log({ email, password });
}
}
return (
<form onSubmit={handleSubmit}>
<input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
{errors.email && <p>{errors.email}</p>}
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
{errors.password && <p>{errors.password}</p>}
<button type="submit">Войти</button>
</form>
);
}
Работает, но представьте форму регистрации на 8 полей: 8 состояний, 8 обработчиков onChange, длинная функция проверки, ручной сброс, флаг «отправляется»... К тому же каждое нажатие клавиши перерисовывает всю форму. Именно эти проблемы решает react-hook-form.
Установка библиотек
Установим сразу всё, что понадобится в уроке:
npm install react-hook-form yup @hookform/resolvers react-toastify
| Пакет | Зачем |
|---|---|
react-hook-form |
Управляет формой: значения, ошибки, отправка, сброс |
yup |
Описывает правила проверки данных (схему) |
@hookform/resolvers |
«Переходник», который соединяет react-hook-form со схемой yup |
react-toastify |
Всплывающие уведомления (тосты) |
react-hook-form: основы
Главный инструмент библиотеки — хук useForm. Он возвращает объект с функциями для работы с формой.
import { useForm } from 'react-hook-form';
function LoginForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
function onSubmit(data) {
console.log(data); // { email: '...', password: '...' }
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email', { required: 'Введите email' })} placeholder="Email" />
{errors.email && <p className="error">{errors.email.message}</p>}
<input
type="password"
{...register('password', {
required: 'Введите пароль',
minLength: { value: 8, message: 'Минимум 8 символов' },
})}
placeholder="Пароль"
/>
{errors.password && <p className="error">{errors.password.message}</p>}
<button type="submit">Войти</button>
</form>
);
}
export default LoginForm;
Разберём, что здесь происходит.
register — регистрируем поле
register('email') «подключает» поле к форме под именем email. Функция возвращает объект со свойствами name, onChange, onBlur и ref, а {...register('email')} разворачивает их в атрибуты <input>. То есть одна строчка заменяет value, onChange и useState.
Вторым аргументом можно передать встроенные правила проверки:
| Правило | Пример | Что проверяет |
|---|---|---|
required |
required: 'Обязательное поле' |
Поле не пустое |
minLength / maxLength |
minLength: { value: 8, message: '...' } |
Длина строки |
min / max |
min: { value: 18, message: '...' } |
Числовое значение |
pattern |
pattern: { value: /^\S+@\S+$/, message: '...' } |
Регулярное выражение |
validate |
validate: checkName |
Своя функция: возвращает true или текст ошибки |
handleSubmit — отправка
handleSubmit(onSubmit) создаёт обработчик для <form onSubmit>. Он сам вызывает preventDefault, проверяет все поля и:
- если ошибок нет — вызывает вашу
onSubmitс объектом всех значений формы; - если ошибки есть —
onSubmitне вызывается, а ошибки попадают вerrors.
errors — ошибки
formState.errors — объект, где ключи — имена полей с ошибками. У каждой ошибки есть message — текст из правил. errors.email?.message — удобная запись с опциональной цепочкой.
Что ещё умеет useForm
defaultValues(опцияuseForm) — начальные значения полей, важны для форм редактирования;reset()— вернуть форму к начальным значениям, например после успешной отправки;watch('email')— текущее значение поля «на лету» (для счётчика символов, силы пароля);formState.isSubmitting—true, пока выполняется асинхронныйonSubmit, удобно блокировать кнопку.
Валидация с yup
Встроенные правила хороши для простых форм. Но когда правил много, их удобнее описать отдельно — схемой. Схема yup — это описание того, какими должны быть данные: «email — строка, обязательная, в формате email; возраст — число от 14 до 100».
Пишем схему
// schemas/registerSchema.js
import * as yup from 'yup';
export const registerSchema = yup.object({
name: yup
.string()
.trim()
.required('Введите имя')
.min(2, 'Имя слишком короткое'),
email: yup
.string()
.required('Введите email')
.email('Некорректный email'),
age: yup
.number()
.typeError('Введите число')
.required('Укажите возраст')
.min(14, 'Регистрация с 14 лет')
.max(100, 'Проверьте возраст'),
password: yup
.string()
.required('Введите пароль')
.min(8, 'Минимум 8 символов')
.matches(/[A-Z]/, 'Нужна хотя бы одна заглавная латинская буква')
.matches(/\d/, 'Нужна хотя бы одна цифра'),
confirmPassword: yup
.string()
.required('Повторите пароль')
.oneOf([yup.ref('password')], 'Пароли не совпадают'),
agree: yup
.boolean()
.oneOf([true], 'Нужно согласиться с правилами'),
});
Разбор методов:
yup.object({...})— схема объекта: каждое поле формы описано своим правилом.yup.string(),yup.number(),yup.boolean()— тип значения..required('текст')— обязательное поле; в скобках сообщение об ошибке..trim()— убирает пробелы по краям перед проверкой..email()— проверка формата email..min()/.max()— для строк это длина, для чисел — значение..typeError('Введите число')— сообщение, если значение не превратилось в число (например, поле пустое или в нём буквы)..matches(/регулярка/, 'текст')— проверка регулярным выражением (вспомните урок Регулярные выражения).yup.ref('password')— ссылка на значение другого поля;.oneOf([...])— значение должно совпасть с одним из списка. Так проверяется «повторите пароль».
Подключаем схему к форме
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import { registerSchema } from './schemas/registerSchema';
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: yupResolver(registerSchema),
});
Теперь правила в register писать не нужно — register('email') и всё. Проверкой занимается yup, а react-hook-form раскладывает его ошибки по полям.
React-toastify: уведомления
Тост — маленькое всплывающее сообщение в углу экрана: «Сохранено», «Ошибка сети». Оно само исчезает через несколько секунд и не мешает работе, в отличие от alert.
Подключение
ToastContainer — «место», где будут появляться уведомления. Его добавляют один раз в корне приложения:
// App.jsx
import { ToastContainer } from 'react-toastify';
import RegisterForm from './components/RegisterForm';
function App() {
return (
<>
<RegisterForm />
<ToastContainer position="top-right" autoClose={3000} />
</>
);
}
export default App;
В актуальной версии react-toastify стили подключаются автоматически. Если вы работаете со старым проектом и тосты выглядят «голыми», добавьте импорт import 'react-toastify/dist/ReactToastify.css'.
Показ уведомлений
Функцию toast можно вызвать из любого компонента:
import { toast } from 'react-toastify';
toast('Обычное сообщение');
toast.success('Профиль сохранён!');
toast.error('Не удалось отправить форму');
toast.info('Новая версия доступна');
toast.warning('Сессия скоро истечёт');
// с настройками для конкретного тоста
toast.success('Готово', { position: 'bottom-center', autoClose: 1500 });
Особенно удобен toast.promise — он сам показывает «загрузка», «успех» или «ошибка» в зависимости от результата Promise:
toast.promise(saveProfile(data), {
pending: 'Сохраняем...',
success: 'Сохранено!',
error: 'Ошибка сохранения',
});
Собираем полную форму регистрации
Объединим всё: react-hook-form + yup + toastify + асинхронная отправка.
// components/RegisterForm.jsx
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import { toast } from 'react-toastify';
import { registerSchema } from '../schemas/registerSchema';
import styles from './RegisterForm.module.css';
// имитация запроса на сервер
function fakeRegister(data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (data.email === 'taken@example.com') {
reject(new Error('Этот email уже зарегистрирован'));
} else {
resolve({ id: Date.now(), ...data });
}
}, 1500);
});
}
function RegisterForm() {
const {
register,
handleSubmit,
reset,
formState: { errors, isSubmitting },
} = useForm({
resolver: yupResolver(registerSchema),
mode: 'onTouched',
defaultValues: {
name: '',
email: '',
password: '',
confirmPassword: '',
agree: false,
},
});
async function onSubmit(data) {
try {
const user = await fakeRegister(data);
toast.success(`Добро пожаловать, ${user.name}!`);
reset();
} catch (error) {
toast.error(error.message);
}
}
return (
<form className={styles.form} onSubmit={handleSubmit(onSubmit)} noValidate>
<h2>Регистрация</h2>
<label className={styles.field}>
Имя
<input {...register('name')} />
{errors.name && <span className={styles.error}>{errors.name.message}</span>}
</label>
<label className={styles.field}>
Email
<input type="email" {...register('email')} />
{errors.email && <span className={styles.error}>{errors.email.message}</span>}
</label>
<label className={styles.field}>
Возраст
<input type="number" {...register('age')} />
{errors.age && <span className={styles.error}>{errors.age.message}</span>}
</label>
<label className={styles.field}>
Пароль
<input type="password" {...register('password')} />
{errors.password && <span className={styles.error}>{errors.password.message}</span>}
</label>
<label className={styles.field}>
Повторите пароль
<input type="password" {...register('confirmPassword')} />
{errors.confirmPassword && (
<span className={styles.error}>{errors.confirmPassword.message}</span>
)}
</label>
<label className={styles.checkbox}>
<input type="checkbox" {...register('agree')} />
Я согласен с правилами
</label>
{errors.agree && <span className={styles.error}>{errors.agree.message}</span>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Отправляем...' : 'Зарегистрироваться'}
</button>
</form>
);
}
export default RegisterForm;
/* components/RegisterForm.module.css */
.form { max-width: 360px; margin: 40px auto; display: flex; flex-direction: column; gap: 14px; }
.field { display: flex; flex-direction: column; gap: 4px; }
.field input { padding: 8px 10px; border: 1px solid #d1d5db; border-radius: 8px; }
.checkbox { display: flex; gap: 8px; align-items: center; }
.error { color: #dc2626; font-size: 13px; }
Разбор ключевых мест:
resolver: yupResolver(registerSchema)— вся проверка по схеме yup.mode: 'onTouched'— ошибка появится, когда пользователь ушёл с поля, а не сразу при открытии формы.defaultValues— начальные значения полей;reset()изuseFormвозвращает форму к ним. Полеageбез начального значения: пустое поле не превратится в число, и yup покажет «Введите число».noValidateна<form>отключает встроенные браузерные подсказки, чтобы показывались наши сообщения.onSubmit—async: пока Promise не завершился,formState.isSubmittingравенtrue, кнопка заблокирована, и пользователь не отправит форму дважды.try/catch— успех показываемtoast.success, ошибку сервера —toast.error(повторите урок try, catch, finally).- Попробуйте зарегистрировать
taken@example.com— увидите красный тост.
Вместо fakeRegister в реальном проекте будет запрос на сервер через fetch или axios — это сделаем в следующем уроке.
Типичные ошибки
registerбез spread.<input register('email') />— ошибка синтаксиса. Нужно{...register('email')}.onSubmit={onSubmit}вместоonSubmit={handleSubmit(onSubmit)}— проверка не сработает, страница перезагрузится.- Имя в схеме и в
registerне совпадает —confirmPasswordв одном месте иpasswordConfirmв другом. Ошибка не покажется. - Забыли
ToastContainer—toast.successничего не покажет. - Два
ToastContainerв разных компонентах — тосты будут дублироваться. - Числовое поле без
typeError— пользователь увидит непонятное английское сообщение yup о типе.
Практика
- Форма входа на react-hook-form. Поля email и пароль со встроенными правилами (
required,pattern,minLength). Ожидаемый результат: ошибки под полями, при успехе данные в консоли. - Схема yup. Перепишите форму входа на
yupResolver. Добавьте чекбокс «Запомнить меня» сdefaultValues: { remember: true }. - Тосты. При успешном входе показывайте
toast.success, а если email равенblocked@example.com—toast.error('Аккаунт заблокирован'). Кнопка блокируется на время «запроса» (setTimeoutна 1 секунду). - Обратная связь. Форма «Связаться с нами»: имя (2–50 символов), телефон (регулярка для формата
+7XXXXXXXXXX), тема (selectс 3 вариантами, обязателен), сообщение (от 10 символов). Под полем сообщения — счётчик символов черезwatch. После отправки —reset()и тост. - Полная регистрация. Повторите
RegisterFormиз урока и добавьте поле «Дата рождения» (yup.date()с проверкой, что пользователю есть 14 лет) и отображение силы пароля (слабый/средний/сильный) черезwatch('password').
Итоги
- Формы на
useStateбыстро разрастаются; react-hook-form берёт на себя значения, ошибки, отправку и сброс. useForm()→register(подключить поле),handleSubmit(отправка с проверкой),formState.errors(ошибки),reset,watch,isSubmitting.- Правила удобно описывать схемой yup:
yup.object({ поле: yup.string().required() })и подключать черезyupResolver. - Полезные методы yup:
required,email,min/max,matches,typeError,oneOf([yup.ref('password')]). - react-toastify: один
ToastContainerв корне, затемtoast.success/error/info/promiseиз любого места. - Проверка на клиенте — для удобства; сервер обязан проверять данные сам.