Пошук уроків, статей та іншого контенту
Покажете індикатори виконання мутації та заблокуєте повторне надсилання форми під час запиту.
Мутація — це операція, яка змінює дані: створення запису, оновлення профілю, видалення елемента тощо. Поки сервер обробляє запит, користувачеві потрібно показати, що операція ще триває.
Без індикатора завантаження користувач може:
повторно натиснути кнопку;
відправити форму кілька разів;
вирішити, що застосунок не відповідає;
змінити поля під час виконання запиту.
У Next.js для форм із Server Actions зручно використовувати:
useActionState — для запуску Server Action і зберігання її результату;
useFormStatus — для отримання стану поточної відправки форми;
атрибут disabled — для блокування повторної взаємодії.
loading.tsxпризначений переважно для завантаження сторінок і сегментів маршруту. Для стану мутації форми використовуйте стан конкретної форми.
Створимо Server Action, яка перевіряє назву завдання та імітує збереження даних.
Файл app/actions.ts:
"use server";
import { revalidatePath } from "next/cache";
export type TaskFormState = {
message: string;
success: boolean;
};
export async function createTask(
previousState: TaskFormState,
formData: FormData
): Promise<TaskFormState> {
const title = String(formData.get("title") ?? "").trim();
if (!title) {
return {
message: "Введіть назву завдання.",
success: false,
};
}
if (title.length < 3) {
return {
message: "Назва завдання має містити щонайменше 3 символи.",
success: false,
};
}
// Імітація повільного запиту до бази даних
await new Promise((resolve) => setTimeout(resolve, 1500));
// Тут у реальному застосунку буде запис до бази даних
console.log(`Створено завдання: ${title}`);
revalidatePath("/tasks");
return {
message: `Завдання «${title}» створено.`,
success: true,
};
}previousState містить попередній результат Server Action. formData містить значення полів форми.
У реальному застосунку замість setTimeout буде операція запису до бази даних або виклик іншого сервісу.
Файл app/tasks/TaskForm.tsx:
"use client";
import { useActionState } from "react";
import { useFormStatus } from "react-dom";
import {
createTask,
type TaskFormState,
} from "../actions";
const initialState: TaskFormState = {
message: "",
success: false,
};
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Збереження..." : "Створити завдання"}
</button>
);
}
export default function TaskForm() {
const [state, formAction] = useActionState(
createTask,
initialState
);
const { pending } = useFormStatus();
return (
<form action={formAction}>
<fieldset disabled={pending}>
<label htmlFor="title">Назва завдання</label>
<input
id="title"
name="title"
type="text"
placeholder="Наприклад, підготувати звіт"
required
/>
<SubmitButton />
</fieldset>
{pending && (
<p role="status" aria-live="polite">
Виконуємо запит...
</p>
)}
{!pending && state.message && (
<p
role="status"
aria-live="polite"
className={state.success ? "success" : "error"}
>
{state.message}
</p>
)}
</form>
);
}Компонент сторінки може підключити форму так:
import TaskForm from "./TaskForm";
export default function TasksPage() {
return (
<main>
<h1>Завдання</h1>
<TaskForm />
</main>
);
}pendinguseFormStatus повертає об’єкт зі статусом поточної відправки форми. Найважливіша властивість — pending.
Її значення:
false — форма не виконує Server Action;
true — Server Action виконується.
Під час pending компонент:
змінює текст кнопки на Збереження...;
блокує кнопку;
блокує всі поля всередині fieldset;
показує повідомлення про виконання запиту;
не показує старе повідомлення про успішне завершення.
aria-live="polite" повідомляє засобам доступності, що текст статусу змінився.
useFormStatus потрібно використовувати в дочірньому компонентіuseFormStatus отримує стан найближчої батьківської форми. Тому цей хук не можна використовувати безпосередньо в компоненті, який повертає <form> і очікувати, що він прочитає стан саме цієї форми.
Правильна структура:
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Надсилання..." : "Надіслати"}
</button>
);
}
function Form() {
return (
<form action={someAction}>
<SubmitButton />
</form>
);
}SubmitButton є дочірнім компонентом <form>, тому useFormStatus працює коректно.
Саме disabled={pending} не дає користувачеві повторно натискати кнопку, поки запит ще виконується.
Для блокування всієї форми можна використати fieldset:
<fieldset disabled={pending}>
<input name="title" />
<button type="submit">
Зберегти
</button>
</fieldset>Браузер автоматично вимикає елементи форми всередині такого fieldset.
Це покращує поведінку інтерфейсу, але не замінює захист на сервері. Серверна мутація має бути готовою до повторного запиту, оскільки запити можуть дублюватися через оновлення сторінки, повторну відправку з іншого клієнта або мережеві проблеми.
Server Action може повернути стан із повідомленням про помилку:
return {
message: "Введіть назву завдання.",
success: false,
};Після завершення дії useActionState оновить значення state, і компонент покаже це повідомлення.
Під час помилки валідації форма знову стане доступною, тому користувач зможе виправити дані та повторити спробу.
Під час успішної мутації можна:
показати повідомлення;
очистити форму;
оновити дані через revalidatePath;
перенаправити користувача на іншу сторінку.
Вибір залежить від сценарію форми.
loading.tsx для кнопки мутаціїloading.tsx не є локальним станом кнопки. Він показує резервний інтерфейс під час завантаження маршруту або його частини.
Для стану відправки форми використовуйте useFormStatus.
useFormStatus поза формоюТакий компонент не матиме доступу до статусу потрібної форми. Переконайтеся, що компонент із useFormStatus рендериться всередині <form>.
Якщо заблокувати лише кнопку, користувач може спробувати відправити форму клавішею Enter або змінювати поля під час запиту.
Для повного блокування інтерфейсу форми використовуйте fieldset disabled={pending}.
Зміна напису на кнопці корисна, але її може бути недостатньо для користувачів допоміжних технологій. Додайте повідомлення з role="status" і aria-live="polite".
disabled захищає серверdisabled впливає лише на поточний інтерфейс. Він не захищає Server Action від повторних запитів, надісланих іншим способом.
Серверна логіка має коректно обробляти повторне виконання мутації.
Для стану мутації форми використовуйте useFormStatus.
Властивість pending показує, чи виконується Server Action.
Під час pending змінюйте текст кнопки та блокуйте форму.
useFormStatus потрібно використовувати в дочірньому компоненті форми.
useActionState зручно використовувати для отримання результату мутації та повідомлень про помилки.
aria-live допомагає оголошувати зміни стану користувачам допоміжних технологій.
Блокування форми на клієнті покращує UX, але не замінює серверну обробку повторних запитів.