AI Структура против хаоса — практическая валидация форм с помощью Zod

AI

Редактор
Регистрация
23 Август 2023
Сообщения
3 641
Лучшие ответы
0
Реакции
0
Баллы
243
Offline
#1


Всем привет, с вами Артем Леванов, Front Lead в компании WebRise.

В прошлой статье мы разобрали, как навести порядок в создании форм — выделили примитивы, ячейки и типовые поля.

Следующая проблема, с которой сталкивается любая форма — валидация.

Формы могут быть красивыми и структурными, но без единого подхода к валидации они быстро превращаются в хаос.

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

Что такое Zod


Zod — это библиотека для декларативного описания структуры данных. Она не просто проверяет значения, а описывает правила так, чтобы они были понятны коду, разработчику, и TypeScript типам одновременно.

Добавить Zod в проект достаточно просто:

1. Устанавливаем зависимость

npm i zod

2. Описываем схему валидации для формы

const LoginFormSchema = z.object({
email: z.string().email('Некорректный email'),
password: z.string().min(2, 'Пароль слишком короткий'),
});

type LoginFormType = z.infer<typeof LoginFormSchema>;

3. Подключаем схему через zodResolver, предоставляемый React Hook Form

const methods = useForm<LoginFormType>({
resolver: zodResolver(LoginFormSchema),
mode: 'onSubmit',
});

После этого форма получает автоматические типы, подсветку ошибок и единое место, где живут все правила. Полную версию формы можно посмотреть в демо на codeSanbox

Проблемы валидации и их решения на основе Zod

Громоздкий код валидации


Без использования схем код валидации часто оказывается разбросанным по проекту и выглядит примерно так:

const onSubmit = (data: any) => {
const errors: Record<string, string> = {};

if (!data.email) {
errors.email = 'Email обязателен';
} else if (!data.email.includes('@')) {
errors.email = 'Некорректный email';
}

if (!data.password) {
errors.password = 'Пароль обязателен';
} else if (data.password.length < 8) {
errors.password = 'Минимум 8 символов';
}

if (Object.keys(errors).length > 0) {
setFormErrors(errors);
return;
}

// отправка данных;
};

Zod позволяет писать код компактнее и строго в определенном месте.

const LoginFormSchema = z.object({
email: z.string().min(1, 'Email обязателен').email('Некорректный email'),
password: z.string().min(1, 'Пароль обязателен').min(8, 'Минимум 8 символов'),
});
Отсутствие строгой типизации данных


Как пример, пользователь может ввести в поле “двадцать“ и типизация TypeScript уже не будет работать

interface FormData {
email: string;
age: number; // но пользователь может ввести не число! Да и input всегда возвращает строку
}

const { register, handleSubmit } = useForm<FormData>();

// В UI:
<input {...register('age')} />; // пользователь ввёл строкой "двадцать"

// На сервере: parseInt("двадцать") → NaN


Zod решает эту проблему и подсвечивает пользователю ошибку:

const RegistrationFormSchema = z.object({
email: z.string().email('Некорректный email'),
age: z.coerce.number({
invalid_type_error: 'Возраст должен быть числом', // если тип не `number`
}),
password: z.string().min(2, 'Пароль слишком короткий'),
});

Схема сама приводит строку к числу и выдаёт понятную ошибку, если приведение невозможно.

Подготовка данных перед отправкой на бэк


Как правило, данные перед отправкой на бэк надо подготовить. Обрезать пробелы спереди и сзади, привести к строчному виду и т.д. Это требует отдельного места, где этот процесс проиcходит, обычно это делают в функции onSubmit, которая сильно раздувается

Zod позволяет прямо в схеме преобразовать данные до нужного вида

name: z.string().transform(str => str.trim())

onSubmit остаётся чистым и делает то, что должен — отправляет данные.

Сложности асинхронной валидации


Проверка асинхронной валидации требует добавления различных состояний и обработчиков.

const [isChecking, setIsChecking] = useState(false);
const [emailError, setEmailError] = useState('');

const checkEmailAvailability = async (email: string) => {
const res = await fetch(`/api/check-email?email=${email}`);
if (!res.ok) throw new Error('Ошибка проверки логина');
return res.json() as Promise<boolean>;
};

const checkEmail = async (value: string) => {
setIsChecking(true);
const isAvailable = await checkEmailAvailability(value);
setIsChecking(false);
if (!isAvailable) {
setEmailError('Логин уже занят');
} else {
setEmailError('');
}
};

<input
{...register('email', {
onChange: (e) => checkEmail(e.target.value),
})}
/>;
{isChecking && <span>Проверяем...</span>;}
{emailError && <span>{emailError}</span>;}

При использовании zod, схема становится единым источником истины для синхронных и асинхронных проверок, а связка React Hook Form + zodResolver берут на себя статусы и ошибки, не требуя ручных состояний.

const RegistrationFormSchema = z.object({
email: z
.string()
.min(1, 'Укажите email')
.email('Некорректный email')
.refine(
async (value) => {
const isAvailable = await checkEmailAvailability(value);
return isAvailable;
},
{ message: 'Логин уже занят' }
),
password: z.string().min(8, 'Минимум 8 символов'),
});

Схема остаётся декларативной, а RHF берет на себя управление состояниями. Стоит отметить, что вся валидация становится асинхронной и запускать ее лучше через mode submit или использовать debounce.

Валидации взаимосвязанных полей


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

const onSubmit = (data: any) => {
const errors: Record<string, string> = {};
const regexPhone = /^\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}$/;

if (data.hasPhone) {
if (!data.phone) {
errors.phone = 'Укажите телефон';
} else if (!regexPhone.test(data.phone)) {
errors.phone = 'Неверный формат';
}
}

if (data.age > 18 && !data.documentId) {
errors.documentId= 'Укажите номер документа';
}

if (Object.keys(errors).length) {
setFormErrors(errors);
return;
}

// отправка данных формы
};

Zod позволяет вынести эту логику в схему и предоставляет инструменты (refine, superRefine) для описания правил валидации между полями


const regexPhone = /^\\+7 \\(\\d{3}\\) \\d{3}-\\d{2}-\\d{2}$/;
const RegistrationFormSchema= z.object({
hasPhone: z.boolean().optional(),
email: z.string().min(1, 'Укажите email').email('Некорректный email'),
phone: z.string().regex(regexPhone, 'Телефон должен быть в формате +7 (999) 999-99-99').optional(),
age: z.coerce
.number({
invalid_type_error: 'Возраст должен быть числом',
})
.min(1, 'Укажите возраст'),
documentId: z.coerce
.number({
invalid_type_error: 'Номер документа должен быть числом',
})
.optional(),
})
// Валидация проходит, если телефон не требуется или он указан.
.refine((data) => !data.hasPhone || !!data.phone, {
path: ['phone'],
message: 'Укажите телефон',
})
// Валидация проходит, если возраст не больше 18 или указан номер документа.
.refine((data) => !(data.age > 18) || !!data.documentId, {
path: ['documentId'],
message: 'Укажите номер документа',
});
Валидация динамических полей


Формы, в которых пользователь может добавлять поля “на лету” (например, список адресов, телефонов или документов), требуют особого внимания. Разработчик сталкивается с целым набором технических задач:


  • описание типов


  • подготовка ui для удаления и выведения новых полей


  • написание системы редактирования проверок валидации “на лету“


  • серверные проверки

Zod в связке с React Hook Form эффективно решает эти проблемы и снижает объём и сложность кода.

addresses: z.array(
z.object({
zipCode: z.string().min(1, 'Укажите индекс'),
city: z.string().min(1, 'Укажите город'),
})
),

Полный пример работы такой формы можно посмотреть демке

Таким образом, Zod позволяет держать типы и валидацию в одном месте, а RHF — эффективно управлять динамическими массивами полей

Преимущества Zod


Помимо решения стандартных проблем валидации, Zod дает дополнительные преимущества.

Автоматическое выведение типов из схемы


Создавая схему, мы одновременно описываем структуру данных и получаем типы:

const UserSchema = z.object({
id: z.number(),
name: z.string(),
});
type User = z.infer<typeof UserSchema>; // готовый тип
Повторное использование схем


Схемы удобно переиспользовать между фронтом и бэком, или между разными похожими формами. Например формы редактирования и создания пользователя, где состав полей может отличаться только на id

const UserSchema = z.object({ ... });
const CreateUserSchema = UserSchema.omit({ id: true });
Валидация входящих данных в runtime


TypeScript защищает только на этапе компиляции.

Zod же позволяет валидировать данные в рантайме, например — ответы API, query-параметры или содержимое localStorage

const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});

fetch('/api/user/1')
.then((res) => res.json())
.then((data) => {
const user = UserSchema.parse(data); // проверка в рантайме
console.log(user.name);
})
.catch((err) => {
if (err instanceof z.ZodError) {
console.error('Некорректные данные от API:', err.errors);
} else {
console.error('Ошибка запроса:', err);
}
});

Итоги


Zod помогает привести валидацию к предсказуемой и декларативной системе: структура данных, типы и правила находятся в одном месте, а React Hook Form берёт на себя техническую часть работы с формой. Такой подход отлично масштабируется и остаётся управляемым даже при усложнении сценариев — динамические поля, асинхронные проверки, зависимости между значениями.

В первой статье мы стандартизировали компонентный слой форм — примитивы, ячейки и типовые поля. Добавив Zod как формальный язык для описания валидации, мы получили завершённую модель, где UI и правила валидации описаны единообразно и могут использоваться совместно. Такой подход облегчает поддержку, снижает количество ручного кода и создаёт фундамент для автоматизации.

По вопросам, телеграм @webrise1
 
Яндекс.Метрика Рейтинг@Mail.ru
Сверху Снизу