Пошук уроків, статей та іншого контенту
Створите новий запис у базі даних, обробивши дані форми та результат мутації.
Server Action — це асинхронна функція, яка виконується на сервері, але може бути викликана безпосередньо з HTML-форми або клієнтського компонента.
Під час створення запису Server Action зазвичай виконує такі кроки:
Отримує дані з FormData.
Перевіряє та нормалізує введені значення.
Створює запис у базі даних.
Оновлює кеш потрібного маршруту.
Повертає результат операції у форму.
У цьому прикладі створимо публікацію з полями title і content.
Для прикладу використаємо Prisma та SQLite.
Файл prisma/schema.prisma:
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Post {
id Int @id @default(autoincrement())
title String
content String
createdAt DateTime @default(now())
}Файл .env:
DATABASE_URL="file:./dev.db"Встановіть Prisma та створіть базу даних:
npm install prisma @prisma/client
npx prisma migrate dev --name create_postsСтворимо єдиний екземпляр Prisma Client. Це особливо важливо в режимі розробки, де Next.js може багаторазово перезавантажувати модулі.
Файл lib/prisma.ts:
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma =
globalForPrisma.prisma ??
new PrismaClient();
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}Створимо окремий файл для дії.
Файл app/actions/posts.ts:
"use server";
import { revalidatePath } from "next/cache";
import { prisma } from "@/lib/prisma";
export type CreatePostState = {
ok: boolean;
message: string;
fieldErrors?: {
title?: string;
content?: string;
};
postId?: number;
};
export async function createPost(
_previousState: CreatePostState,
formData: FormData,
): Promise<CreatePostState> {
const titleValue = formData.get("title");
const contentValue = formData.get("content");
if (
typeof titleValue !== "string" ||
typeof contentValue !== "string"
) {
return {
ok: false,
message: "Некоректні дані форми.",
};
}
const title = titleValue.trim();
const content = contentValue.trim();
const fieldErrors: CreatePostState["fieldErrors"] = {};
if (title.length < 3) {
fieldErrors.title = "Заголовок має містити щонайменше 3 символи.";
}
if (content.length < 10) {
fieldErrors.content = "Текст має містити щонайменше 10 символів.";
}
if (Object.keys(fieldErrors).length > 0) {
return {
ok: false,
message: "Перевірте правильність заповнення форми.",
fieldErrors,
};
}
try {
const post = await prisma.post.create({
data: {
title,
content,
},
});
revalidatePath("/posts");
return {
ok: true,
message: "Публікацію успішно створено.",
postId: post.id,
};
} catch {
return {
ok: false,
message: "Не вдалося створити публікацію. Спробуйте ще раз.",
};
}
}Директива "use server" на початку файлу повідомляє Next.js, що функції з цього модуля мають виконуватися на сервері.
Метод formData.get() повертає значення типу FormDataEntryValue | null. Це значення може бути рядком або об'єктом File, тому перед використанням потрібно перевірити його тип:
const titleValue = formData.get("title");
if (typeof titleValue !== "string") {
// Значення відсутнє або має неправильний тип
}Після цього значення можна нормалізувати:
const title = titleValue.trim();Перевірка на сервері обов'язкова. Клієнтська валідація покращує взаємодію з формою, але не може бути єдиним захистом.
Server Action повертає об'єкт із результатом операції:
return {
ok: true,
message: "Публікацію успішно створено.",
postId: post.id,
};Або помилку:
return {
ok: false,
message: "Перевірте правильність заповнення форми.",
fieldErrors: {
title: "Заголовок надто короткий.",
},
};Такий результат можна показати користувачу у формі.
Для відображення результату використаємо useActionState. Цей хук зберігає стан Server Action після кожного надсилання форми.
Файл app/posts/new/PostForm.tsx:
"use client";
import { useActionState } from "react";
import { useFormStatus } from "react-dom";
import {
createPost,
type CreatePostState,
} from "@/app/actions/posts";
const initialState: CreatePostState = {
ok: false,
message: "",
};
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Збереження..." : "Створити публікацію"}
</button>
);
}
export default function PostForm() {
const [state, formAction] = useActionState(
createPost,
initialState,
);
return (
<form action={formAction}>
<div>
<label htmlFor="title">Заголовок</label>
<input
id="title"
name="title"
type="text"
aria-invalid={Boolean(state.fieldErrors?.title)}
/>
{state.fieldErrors?.title && (
<p role="alert">{state.fieldErrors.title}</p>
)}
</div>
<div>
<label htmlFor="content">Текст</label>
<textarea
id="content"
name="content"
rows={8}
aria-invalid={Boolean(state.fieldErrors?.content)}
/>
{state.fieldErrors?.content && (
<p role="alert">{state.fieldErrors.content}</p>
)}
</div>
<SubmitButton />
{state.message && (
<p
role="status"
aria-live="polite"
>
{state.message}
</p>
)}
</form>
);
}Ключові частини цього компонента:
name="title" і name="content" визначають ключі в FormData.
action={formAction} передає форму Server Action.
useActionState отримує результат дії.
useFormStatus показує стан виконання форми.
disabled={pending} не дозволяє відправити форму повторно під час мутації.
Файл app/posts/new/page.tsx:
import PostForm from "./PostForm";
export default function NewPostPage() {
return (
<main>
<h1>Нова публікація</h1>
<PostForm />
</main>
);
}Тепер форма буде доступна за адресою /posts/new.
Після надсилання:
Браузер сформує FormData.
Next.js викличе createPost на сервері.
Server Action перевірить поля.
Prisma створить запис у базі даних.
revalidatePath("/posts") позначить сторінку зі списком публікацій як таку, що потребує оновлення.
Результат буде переданий назад у PostForm.
Після створення запису сторінка зі списком даних може містити застарілий результат. Для її оновлення використовується revalidatePath:
revalidatePath("/posts");Цей виклик потрібно робити після успішної мутації, а не після помилки валідації.
Наприклад:
const post = await prisma.post.create({
data: {
title,
content,
},
});
revalidatePath("/posts");Якщо список публікацій використовує інший маршрут, потрібно вказати саме його:
revalidatePath("/articles");Помилки бази даних не варто безпосередньо показувати користувачу. Вони можуть містити внутрішню інформацію про структуру застосунку або бази.
У Server Action краще:
записати деталі помилки в серверний лог;
повернути користувачу загальне повідомлення;
не викликати revalidatePath, якщо запис не було створено.
Наприклад:
try {
const post = await prisma.post.create({
data: {
title,
content,
},
});
revalidatePath("/posts");
return {
ok: true,
message: "Публікацію успішно створено.",
postId: post.id,
};
} catch (error) {
console.error("Помилка створення публікації:", error);
return {
ok: false,
message: "Не вдалося створити публікацію.",
};
}Клієнтський компонент не повинен напряму створювати підключення до бази даних або виконувати Prisma-запити.
Неправильний підхід:
"use client";
// Так робити не можна:
// prisma.post.create(...)Запит до бази даних має виконуватися в Server Action або іншому серверному коді.
nameБез атрибута name значення поля не потрапить до FormData:
<input id="title" />Правильно:
<input id="title" name="title" />Атрибути required, minLength та перевірки в клієнтському коді корисні для інтерфейсу, але їх можна обійти. Перевіряйте дані ще раз у Server Action.
revalidatePath до записуЯкщо оновити кеш до виконання мутації, а сама мутація завершиться помилкою, сторінка може бути оновлена без нового запису.
Викликайте revalidatePath тільки після успішного create.
Користувач може натиснути кнопку кілька разів і створити дублікати. useFormStatus дозволяє тимчасово заблокувати кнопку:
const { pending } = useFormStatus();
<button type="submit" disabled={pending}>
{pending ? "Збереження..." : "Створити"}
</button>Server Action виконується на сервері та може напряму працювати з базою даних.
Дані форми отримуються через FormData.
Вхідні значення потрібно перевіряти та нормалізувати на сервері.
Результат мутації можна повернути у формі через useActionState.
Після успішного створення запису використовуйте revalidatePath, щоб оновити кеш сторінки.
Помилки бази даних потрібно обробляти на сервері та не показувати користувачу внутрішні деталі.