Пошук уроків, статей та іншого контенту
Застосуєте Zod для схем, повідомлень про помилки та безпечного розбору даних форми.
Server Action отримує дані форми на сервері, але ці дані все одно потрібно вважати ненадійними. Користувач може:
вимкнути HTML-валідацію;
надіслати запит вручну;
передати неправильний тип або порожнє значення;
змінити назви чи значення полів.
Атрибути required, type="email" і minLength покращують взаємодію у браузері, але не замінюють серверну перевірку.
Для валідації використаємо Zod. Схема Zod описує очікувану структуру даних і правила для кожного поля.
Встановіть пакет:
npm install zodСтворимо форму з трьома полями:
ім’я;
електронна пошта;
повідомлення.
Файл app/contact/actions.ts:
"use server";
import { z } from "zod";
const requiredText = (emptyMessage: string, maxLength: number) =>
z.preprocess(
(value) => (typeof value === "string" ? value.trim() : ""),
z
.string()
.min(1, emptyMessage)
.max(maxLength, `Максимум ${maxLength} символів`)
);
const contactSchema = z.object({
name: requiredText("Введіть ім’я", 80),
email: z.preprocess(
(value) => (typeof value === "string" ? value.trim() : ""),
z
.string()
.min(1, "Введіть електронну пошту")
.email("Введіть коректну електронну пошту")
),
message: requiredText("Введіть повідомлення", 1000),
});
type FieldErrors = {
name?: string[];
email?: string[];
message?: string[];
};
export type FormState = {
success: boolean;
message: string;
errors?: FieldErrors;
};
export const initialState: FormState = {
success: false,
message: "",
errors: {},
};
export async function submitContact(
previousState: FormState,
formData: FormData
): Promise<FormState> {
const rawData = {
name: formData.get("name"),
email: formData.get("email"),
message: formData.get("message"),
};
const result = contactSchema.safeParse(rawData);
if (!result.success) {
const fieldErrors = result.error.flatten().fieldErrors;
return {
success: false,
message: "Перевірте дані форми",
errors: fieldErrors,
};
}
const data = result.data;
// Тут можна зберегти data в базу даних або надіслати повідомлення.
console.log("Нове повідомлення:", data);
return {
success: true,
message: "Повідомлення успішно надіслано",
errors: {},
};
}z.object() описує об’єкт із полями.
z.string() перевіряє, що значення є рядком.
z.preprocess() нормалізує значення до основної валідації. У прикладі:
значення обрізається за допомогою trim();
нестрокове або відсутнє значення перетворюється на порожній рядок.
Методи .min() і .max() перевіряють довжину рядка та повертають задані повідомлення про помилки.
Метод .email() перевіряє формат електронної пошти.
safeParse замість parseZod має два поширені способи перевірки:
const result = schema.safeParse(data);і:
const data = schema.parse(input);parse() повертає розібрані дані, але викидає виняток, якщо перевірка не пройдена.
safeParse() не викидає виняток. Він повертає об’єкт із властивістю success:
const result = contactSchema.safeParse(rawData);
if (result.success) {
// result.data містить перевірені дані
} else {
// result.error містить інформацію про помилки
}Для даних форми зазвичай зручніше використовувати safeParse(), оскільки помилка введення є очікуваною ситуацією, а не винятковою помилкою програми.
Виклик:
result.error.flatten().fieldErrorsперетворює помилки на об’єкт, зручний для відображення у формі:
{
name: ["Введіть ім’я"],
email: ["Введіть коректну електронну пошту"]
}Одне поле може мати кілька помилок, тому значенням кожної властивості є масив рядків.
Наприклад, тип помилок може виглядати так:
type FieldErrors = {
name?: string[];
email?: string[];
message?: string[];
};Створимо клієнтський компонент app/contact/ContactForm.tsx:
"use client";
import { useActionState } from "react";
import {
initialState,
submitContact,
} from "./actions";
export default function ContactForm() {
const [state, formAction, isPending] = useActionState(
submitContact,
initialState
);
return (
<form action={formAction}>
<div>
<label htmlFor="name">Ім’я</label>
<input
id="name"
name="name"
type="text"
aria-invalid={Boolean(state.errors?.name)}
disabled={isPending}
/>
{state.errors?.name?.map((error) => (
<p key={error} role="alert">
{error}
</p>
))}
</div>
<div>
<label htmlFor="email">Електронна пошта</label>
<input
id="email"
name="email"
type="email"
aria-invalid={Boolean(state.errors?.email)}
disabled={isPending}
/>
{state.errors?.email?.map((error) => (
<p key={error} role="alert">
{error}
</p>
))}
</div>
<div>
<label htmlFor="message">Повідомлення</label>
<textarea
id="message"
name="message"
rows={5}
aria-invalid={Boolean(state.errors?.message)}
disabled={isPending}
/>
{state.errors?.message?.map((error) => (
<p key={error} role="alert">
{error}
</p>
))}
</div>
<button type="submit" disabled={isPending}>
{isPending ? "Надсилання..." : "Надіслати"}
</button>
{state.message && (
<p role="status">
{state.message}
</p>
)}
</form>
);
}Підключимо компонент у сторінці app/contact/page.tsx:
import ContactForm from "./ContactForm";
export default function ContactPage() {
return (
<main>
<h1>Зворотний зв’язок</h1>
<ContactForm />
</main>
);
}Користувач заповнює форму.
formAction передає дані до Server Action.
Server Action отримує об’єкт FormData.
Із FormData створюється об’єкт rawData.
Zod перевіряє цей об’єкт.
У разі помилки Action повертає errors.
useActionState оновлює стан компонента.
Компонент показує помилки біля відповідних полів.
Якщо валідація успішна, Action виконує серверну операцію та повертає повідомлення про успіх.
FormDataFormData.get() може повернути:
рядок;
File;
null, якщо поля немає.
Тому не варто безпосередньо припускати, що всі значення є коректними рядками:
const email = formData.get("email");
// Небезпечно покладатися лише на це припущення
sendEmail(email as string);Краще передати значення до Zod:
const rawData = {
email: formData.get("email"),
};
const result = emailSchema.safeParse(rawData);
if (!result.success) {
// Дані не пройшли перевірку
}
const validData = result.data;Після успішного safeParse() result.data містить дані, які пройшли схему.
Схему можна експортувати та використовувати в інших серверних функціях або тестах:
export const contactSchema = z.object({
name: requiredText("Введіть ім’я", 80),
email: z.preprocess(
(value) => (typeof value === "string" ? value.trim() : ""),
z
.string()
.min(1, "Введіть електронну пошту")
.email("Введіть коректну електронну пошту")
),
message: requiredText("Введіть повідомлення", 1000),
});Водночас саму перевірку потрібно виконувати всередині Server Action, безпосередньо перед операцією з даними. Не слід покладатися лише на перевірку в клієнтському компоненті.
Zod дає змогу описувати не лише окремі поля, а й залежності між ними. Наприклад, можна перевірити, що два поля збігаються:
import { z } from "zod";
const passwordSchema = z
.object({
password: z.string().min(8, "Пароль має містити щонайменше 8 символів"),
passwordConfirmation: z.string(),
})
.refine(
(data) => data.password === data.passwordConfirmation,
{
message: "Паролі не збігаються",
path: ["passwordConfirmation"],
}
);Властивість path визначає, до якого поля буде прив’язана помилка.
Не всі помилки належать конкретному полю. Наприклад, після успішної валідації може виникнути помилка під час збереження в базу даних.
Для цього можна повернути загальне повідомлення:
return {
success: false,
message: "Не вдалося зберегти повідомлення. Спробуйте ще раз.",
errors: {},
};Помилки введення та помилки виконання операції варто розрізняти:
помилки полів показуються біля відповідних полів;
загальний результат операції показується окремим повідомленням.
HTML-атрибути та клієнтські перевірки не захищають Server Action. Будь-який клієнт може надіслати запит без них.
Валідація Zod має виконуватися всередині Server Action.
parse() без обробки помилкиТакий код може викинути виняток під час звичайного неправильного введення:
const data = contactSchema.parse(rawData);Для форми безпечнішим варіантом є safeParse().
Не передавайте дані з FormData до бази даних або зовнішнього сервісу до перевірки:
const email = formData.get("email");
// Не слід використовувати неперевірене значенняСпочатку виконайте валідацію, а потім використовуйте result.data.
nameДо FormData потрапляють лише поля, які мають атрибут name:
<input id="email" type="email" />У цьому випадку поле не буде доступне як formData.get("email").
Правильний варіант:
<input id="email" name="email" type="email" />Стан, який повертає Server Action, передається до клієнтського компонента. Тому повертайте прості серіалізовані значення:
рядки;
числа;
boolean;
масиви;
звичайні об’єкти.
Не повертайте в стан об’єкти помилок Zod безпосередньо. Перетворюйте їх на повідомлення або масиви рядків через flatten().
Server Actions потрібно валідувати на сервері, навіть якщо форма має HTML-валідацію.
Zod дає змогу описати структуру даних і повідомлення про помилки в одній схемі.
safeParse() повертає результат без винятку та зручно підходить для обробки форм.
result.error.flatten().fieldErrors допомагає прив’язати помилки до окремих полів.
Після успішної перевірки використовуйте result.data, а не початкові сирі значення.
Server Action має повертати серіалізований стан, який клієнтський компонент може відобразити користувачу.