Hello, world!
В этой статье я хочу рассказать о схеме (назовем ее так) работы с формами в React, которая на сегодняшний день кажется мне наиболее эффективной. Эта схема предполагает использование React Hook Form для обработки форм и Zod для валидации пользовательских данных. Применение данной схемы имеет несколько существенных преимуществ по сравнению с использованием других решений или реализацией необходимого функционала вручную. Главными преимуществами являются минимизация количества шаблонного кода и автоматическое выведение типов (type inference).
Для тех, кого интересует только код, вот ссылка на соответствующий репозиторий.
Интересно? Тогда прошу под кат.
В качестве примера разработаем простую форму регистрации, содержащую следующие поля:
- имя пользователя;
- возраст;
- адрес электронной почты;
- пароль;
- подтверждение пароля.
А также индикатор (чекбокс) принятия неких условий использования.
Все поля будут обязательными. О конкретных требованиях к каждому полю поговорим немного позднее.
Подготовка, настройка проекта и создание формы
Создаем шаблон проекта React с поддержкой TypeScript с помощью Vite (для работы с зависимостями я буду использовать Yarn):
# react-hook-form-zod - название проекта
# react-ts - используемый шаблон
yarn create vite react-hook-form-zod --template react-ts
Переходим в созданную директорию, устанавливаем зависимости и запускаем сервер для разработки:
cd react-hook-form-zod
yarn
yarn dev
Наша форма должна быть приятной глазу. Что бы нам использовать для ее стилизации? Как насчет Tailwind CSS? Устанавливаем эту библиотеку в качестве зависимости для разработки:
yarn add -D tailwindcss
Инициализируем ее:
npx tailwindcss init
Импортируем стили tailwind и определяем несколько переиспользуемых (reusable) стилей с помощью директивы @apply в файле src/index.css
:
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.title {
@apply text-2xl text-center font-bold leading-tight tracking-tight text-gray-900;
}
.label {
@apply block mb-2 text-sm font-medium text-gray-900 cursor-pointer;
}
.input {
@apply bg-gray-50 border-none outline outline-1 outline-gray-300 text-gray-900 rounded-md w-full p-2.5 focus-visible:outline-2 focus-visible:outline-blue-500 placeholder:text-sm aria-[invalid="true"]:outline-red-500 aria-[invalid="true"]:outline-2;
transition: outline-color 150ms cubic-bezier(0.4, 0, 0.2, 1);
}
.error {
@apply text-red-600 block text-sm absolute;
}
.btn {
@apply text-white outline-none focus:ring-4 font-medium rounded-md text-sm px-5 py-2.5 text-center transition-colors disabled:opacity-50 disabled:cursor-not-allowed;
}
.btn-primary {
@apply bg-primary-500 hover:bg-primary-700 focus:ring-primary-300 disabled:bg-primary-500;
}
.btn-error {
@apply bg-red-500 hover:bg-red-700 focus:ring-red-300 disabled:bg-red-500;
}
}
Определяем файлы для обработки и расширяем дефолтную цветовую схему tailwind в файле tailwind.config.cjs
:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {
colors: {
primary: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a'
}
}
}
},
plugins: []
}
Наконец, определяем форму в файле src/App.tsx
:
function App() {
return (
<section className='bg-gray-50'>
<div className='flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0'>
<div className='w-full bg-white rounded-lg shadow md:mt-0 sm:max-w-md xl:p-0'>
<div className='p-6 space-y-4 md:space-y-6 sm:p-8'>
<h1 className='title'>Создание аккаунта</h1>
<form className='space-y-7'>
<div className='mb-4'>
<label htmlFor='username' className='label'>
Имя пользователя *
</label>
<input
type='text'
id='username'
className='input'
placeholder='Ваше имя'
/>
</div>
<div className='mb-4'>
<label htmlFor='age' className='label'>
Возраст
</label>
<input
type='number'
id='age'
className='input'
placeholder='От 18 до 65 лет'
/>
</div>
<div>
<label htmlFor='email' className='label'>
Адрес электронной почты *
</label>
<input
type='email'
id='email'
className='input'
placeholder='name@mail.com'
/>
</div>
<div>
<label htmlFor='password' className='label'>
Пароль *
</label>
<input
type='password'
id='password'
placeholder='Не менее 6 символов'
className='input'
/>
</div>
<div>
<label htmlFor='confirmPassword' className='label'>
Подтверждение пароля *
</label>
<input
type='password'
id='confirmPassword'
placeholder='Не менее 6 символов'
className='input'
/>
</div>
<div className='flex items-center relative'>
<input
id='terms'
aria-describedby='terms'
type='checkbox'
className='w-4 h-4 border border-gray-300 bg-gray-50 accent-primary-500 focus:outline-2 focus:outline-primary-500 outline-none'
/>
<label
htmlFor='terms'
className='font-light text-gray-500 text-sm ml-3 cursor-pointer select-none'
>
Я принимаю{' '}
<a
className='font-medium text-primary-500 hover:text-primary-700 focus:text-primary-700 transition-colors outline-none'
href='#'
>
Условия использования
</a>
</label>
</div>
<div className='flex gap-5 justify-center pt-2'>
<button
type='submit'
className='btn btn-primary'
>
Создать аккаунт
</button>
<button
type='button'
className='btn btn-error'
>
Очистить поля
</button>
</div>
</form>
</div>
</div>
</div>
</section>
)
}
export default App
Результат:
Кроме названных выше полей, форма содержит кнопки для отправки формы ("Создать аккаунт") и очистки всех полей ("Очистить поля"). Она выглядит прилично (по крайней мере, на мой "вкус и цвет"