Коротка відповідь
Server Action є точкою входу з недовіреними даними. На сервері прочитайте сесію, перевірте FormData, дозвольте запис, виконайте зміну й поверніть очікуваний результат. useActionState дає feedback і pending. Браузерна валідація не встановлює дозволи чи довіру.
Практичні кроки реалізації
1. Моделюйте очікувані помилки даними
Порожнє ім’я — помилка поля, не несподіваний exception. Поверніть стабільну форму помилки, не текст бази. Збережіть введене й розмістіть feedback поруч із полем зі зв’язками доступності.
2. Встановіть повноваження в action
Користувача визначайте сесією, не hidden userId. Для спільних записів авторизуйте ID і tenant до write. Bound і hidden значення лишаються входами. Адаптер приймає дозволений контекст, не довільного власника.
3. Відокремте pending від повторів
Disabled допомагає проти кліків, але не retries, tabs чи повторів мережі. Для дубльованих ефектів потрібні транзакція, унікальність чи idempotency key. Після write оновіть дані. Перервана відповідь не означає повторне виконання.
Приклад реалізації
Це контракт із двома адаптерами продукту, не наявні модулі репозиторію. requireUser перевіряє сесію, saveDisplayName змінює лише його запис. База й persistence-тести поза прикладом. Показано валідацію та результат useActionState із previous state першим аргументом.
// actions.ts — adapters are supplied by the application.
"use server";
import { requireUser, saveDisplayName } from "./profile.server";
type Result = { error: string | null; saved: boolean };
export async function updateProfile(
_previous: Result, formData: FormData,
): Promise<Result> {
const user = await requireUser();
const raw = formData.get("displayName");
if (typeof raw !== "string" ||
raw.trim().length < 2 || raw.trim().length > 80) {
return { error: "Use 2–80 characters.", saved: false };
}
await saveDisplayName(user.id, raw.trim());
return { error: null, saved: true };
}
"use client";
import { useActionState } from "react";
import { updateProfile } from "./actions";
export function ProfileForm() {
const [state, action, pending] = useActionState(updateProfile, {
error: null, saved: false,
});
return (
<form action={action}>
<label htmlFor="displayName">Display name</label>
<input id="displayName" name="displayName" required
aria-describedby="profile-feedback" />
<p id="profile-feedback" aria-live="polite">
{state.error ?? (state.saved ? "Saved." : "")}
</p>
<button disabled={pending}>{pending ? "Saving…" : "Save"}</button>
</form>
);
}
Компроміси та альтернативи
Server Actions інтегрують форми й App Router. Route Handler потрібен для HTTP-контракту інших клієнтів. Обидва повторно використовують дозволену операцію. Progressive submit можливий, але клієнтський feedback і серверні відмови потребують дизайну.
Поширені помилки
Не cast formData.get у string: можливі null чи File. Не беріть owner із hidden. Disabled не гарантує payment-idempotency. Не повідомляйте saved для всіх catch: запис і notification різні. Статичний експорт не виконує Server Actions.
Перевірка перед випуском
- Action перевіряє сесію й дозволи.
- Поля мають runtime-валідацію.
- Помилки доступні й корисні.
- Повтори мають доменну політику.
- Успіх оновлює авторитетні дані.
Поширені запитання
Чи Server Actions приватні через сервер?
Ні. Це точки входу, що щоразу перевіряють користувача й операцію. Місце виконання не є авторизацією.
Коли валідація має кидати exception?
Очікувані виправні помилки поверніть даними. Exception потрібен для несподіваної відмови поза контрактом форми.
Наступний крок
Перевірте форму з поганим входом, простроченою сесією, забороною й перерваною відповіддю. Feedback точний, повтор не дублює ефект. Кеш додавайте після ясного write-контракту.
Первинні джерела
Пов’язані матеріали та приклади
Потрібно реалізувати це у вашому продукті?
Перегляньте відповідні приклади продуктів або поділіться контекстом розробки вебзастосунку, розширення Chrome, API чи SaaS.