Пошук уроків, статей та іншого контенту
Оновите наявний запис за ідентифікатором і передасте його до Server Action із форми.
Server Action отримує дані форми як об’єкт FormData. Щоб оновити конкретний запис, дія має отримати:
ідентифікатор запису;
нові значення полів;
доступ до сховища даних;
логіку перевірки та оновлення;
перенаправлення або повернення результату після завершення.
Ідентифікатор можна передати через приховане поле форми:
<input type="hidden" name="id" value={post.id} />Таке поле не відображається користувачу, але його значення потрапляє до FormData.
Приховане поле не є захистом. Користувач може змінити його значення вручну, тому в Server Action потрібно перевіряти права доступу до запису.
Розглянемо редагування публікації. Структура файлів може мати такий вигляд:
app/
actions/
posts.ts
posts/
[id]/
edit/
page.tsx
lib/
prisma.ts
prisma/
schema.prismaУ прикладі використовується Prisma як шар доступу до бази даних.
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model Post {
id Int @id @default(autoincrement())
title String
content String
updatedAt DateTime @updatedAt
}Для локального SQLite у файлі .env можна вказати:
DATABASE_URL="file:./dev.db"Після створення або зміни схеми потрібно виконати міграцію:
npx prisma migrate dev --name create_posts// lib/prisma.ts
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: ["error"],
});
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}Збереження клієнта в глобальному об’єкті допомагає уникати створення великої кількості підключень під час гарячого перезавантаження в режимі розробки.
Створимо Server Action у файлі app/actions/posts.ts:
// app/actions/posts.ts
"use server";
import { redirect } from "next/navigation";
import { prisma } from "@/lib/prisma";
export async function updatePost(formData: FormData) {
const idValue = formData.get("id");
const titleValue = formData.get("title");
const contentValue = formData.get("content");
const id = Number(idValue);
const title = String(titleValue ?? "").trim();
const content = String(contentValue ?? "").trim();
if (!Number.isInteger(id) || id <= 0) {
throw new Error("Некоректний ідентифікатор публікації");
}
if (!title || !content) {
throw new Error("Заголовок і текст публікації є обов'язковими");
}
const post = await prisma.post.findUnique({
where: { id },
select: { id: true },
});
if (!post) {
throw new Error("Публікацію не знайдено");
}
await prisma.post.update({
where: { id },
data: {
title,
content,
},
});
redirect(`/posts/${id}`);
}Директива "use server" повідомляє Next.js, що функція повинна виконуватися на сервері.
Сторінка редагування отримує ідентифікатор із динамічного сегмента маршруту:
// app/posts/[id]/edit/page.tsx
import { notFound } from "next/navigation";
import { updatePost } from "@/app/actions/posts";
import { prisma } from "@/lib/prisma";
type EditPostPageProps = {
params: Promise<{
id: string;
}>;
};
export default async function EditPostPage({
params,
}: EditPostPageProps) {
const { id: idValue } = await params;
const id = Number(idValue);
if (!Number.isInteger(id) || id <= 0) {
notFound();
}
const post = await prisma.post.findUnique({
where: { id },
});
if (!post) {
notFound();
}
return (
<main>
<h1>Редагування публікації</h1>
<form action={updatePost}>
<input type="hidden" name="id" value={post.id} />
<div>
<label htmlFor="title">Заголовок</label>
<input
id="title"
name="title"
type="text"
defaultValue={post.title}
required
/>
</div>
<div>
<label htmlFor="content">Текст</label>
<textarea
id="content"
name="content"
defaultValue={post.content}
required
/>
</div>
<button type="submit">Зберегти зміни</button>
</form>
</main>
);
}Коли користувач натискає кнопку, браузер надсилає до Server Action приблизно такі дані:
id=3
title=Оновлений заголовок
content=Оновлений текст публікаціїNext.js автоматично формує об’єкт FormData і передає його функції updatePost.
Послідовність виконання має такий вигляд:
Сторінка отримує id із URL.
За цим ідентифікатором завантажується поточний запис.
Значення запису показуються у формі через defaultValue.
Ідентифікатор додається до форми як приховане поле.
Після відправлення форми викликається updatePost.
Server Action читає значення через formData.get(...).
Значення перевіряються та приводяться до потрібних типів.
prisma.post.update оновлює запис за ідентифікатором.
redirect повертає користувача до сторінки публікації.
Метод update приймає умову пошуку в where і нові значення в data:
await prisma.post.update({
where: { id },
data: {
title,
content,
},
});Поле where визначає, який саме запис потрібно змінити. Поле data містить лише ті властивості, які потрібно оновити.
Після зміни запису сторінка, що показує публікацію, може містити вже закешовані дані. Для очищення кешу можна використати revalidatePath:
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { prisma } from "@/lib/prisma";
export async function updatePost(formData: FormData) {
const id = Number(formData.get("id"));
const title = String(formData.get("title") ?? "").trim();
const content = String(formData.get("content") ?? "").trim();
if (!Number.isInteger(id) || id <= 0) {
throw new Error("Некоректний ідентифікатор");
}
if (!title || !content) {
throw new Error("Заповніть усі поля");
}
await prisma.post.update({
where: { id },
data: {
title,
content,
},
});
revalidatePath(`/posts/${id}`);
redirect(`/posts/${id}`);
}revalidatePath позначає шлях як такий, що потребує повторного отримання даних. Це особливо важливо, якщо сторінка публікації кешується або генерується статично.
bindІноді ідентифікатор не потрібно додавати до FormData. Server Action можна створити за допомогою bind:
// app/actions/posts.ts
"use server";
import { redirect } from "next/navigation";
import { prisma } from "@/lib/prisma";
export async function updatePost(
id: number,
formData: FormData
) {
const title = String(formData.get("title") ?? "").trim();
const content = String(formData.get("content") ?? "").trim();
if (!title || !content) {
throw new Error("Заповніть усі поля");
}
await prisma.post.update({
where: { id },
data: {
title,
content,
},
});
redirect(`/posts/${id}`);
}У компоненті форма підключається так:
<form action={updatePost.bind(null, post.id)}>
<input
name="title"
type="text"
defaultValue={post.title}
required
/>
<textarea
name="content"
defaultValue={post.content}
required
/>
<button type="submit">Зберегти</button>
</form>У цьому випадку post.id передається як перший аргумент Server Action, а FormData залишається другим аргументом.
Обидва підходи коректні:
приховане поле зручне, коли всі дані потрібно отримувати з форми;
bind зручний, коли ідентифікатор уже доступний компоненту та не повинен бути окремим полем форми.
Навіть під час використання bind ідентифікатор потрібно перевіряти на сервері та, за потреби, перевіряти права користувача на редагування запису.
nameДо FormData потрапляють лише елементи форми з атрибутом name:
<input name="title" />Такий елемент не буде переданий до Server Action:
<input id="title" />id потрібен для зв’язку з label, але саме name визначає ключ у FormData.
Значення з FormData завжди мають рядковий тип або тип File:
const idValue = formData.get("id");Перед передаванням до Prisma числовий ідентифікатор потрібно перетворити:
const id = Number(idValue);Після цього слід перевірити, що значення справді є додатним цілим числом.
Перевірка required у HTML покращує взаємодію з користувачем, але її недостатньо. Запит до Server Action можна сформувати без використання браузерної форми, тому важливі перевірки мають виконуватися всередині дії.
if (!title || !content) {
throw new Error("Заповніть усі поля");
}Не можна довіряти id лише тому, що він надійшов із прихованого поля. Потрібно:
перевірити формат ідентифікатора;
переконатися, що запис існує;
перевірити права поточного користувача;
лише після цього виконувати update.
Форму з action={updatePost} можна використовувати в серверному компоненті без "use client". Не потрібно робити всю сторінку клієнтською лише для того, щоб передати форму до Server Action.
Якщо сторінка має кеш, після мутації потрібно викликати revalidatePath або використати інший відповідний механізм інвалідації кешу. Після цього можна виконати redirect.
Server Action може отримувати дані HTML-форми через FormData.
Ідентифікатор запису можна передати прихованим полем із name="id".
Альтернативний варіант — передати ідентифікатор через Function.bind.
Значення з форми потрібно перевіряти та приводити до потрібних типів.
Для оновлення запису використовується операція update із умовою where.
Після зміни даних сторінку можна оновити через revalidatePath.
Ідентифікатор і права доступу потрібно перевіряти на сервері незалежно від способу передавання.