Пошук уроків, статей та іншого контенту
Реалізуєте видалення запису з підтвердженням дії та коректною обробкою результату.
Server Action — це асинхронна серверна функція, яку можна викликати безпосередньо з форми або клієнтського компонента. Для видалення запису Server Action:
отримує ідентифікатор запису;
перевіряє вхідні дані та права доступу;
видаляє запис із бази даних;
оновлює кеш сторінки;
повертає результат операції у форму.
Підтвердження дії виконується у браузері за допомогою window.confirm(). Однак це лише зручність для користувача, а не механізм безпеки. Сервер усе одно повинен самостійно перевірити дані та права доступу.
У прикладі використовується Prisma-клієнт db і модель Post з полем id.
Створимо файл app/posts/actions.ts:
'use server'
import { revalidatePath } from 'next/cache'
import { db } from '@/lib/db'
export type DeletePostState = {
status: 'idle' | 'success' | 'error'
message: string
}
export async function deletePost(
previousState: DeletePostState,
formData: FormData
): Promise<DeletePostState> {
const id = formData.get('id')
if (typeof id !== 'string' || id.length === 0) {
return {
status: 'error',
message: 'Не вдалося визначити запис для видалення.',
}
}
try {
// Тут також потрібно перевірити авторизацію та права користувача.
const result = await db.post.deleteMany({
where: { id },
})
if (result.count === 0) {
return {
status: 'error',
message: 'Запис не знайдено або його вже було видалено.',
}
}
revalidatePath('/posts')
return {
status: 'success',
message: 'Запис успішно видалено.',
}
} catch {
return {
status: 'error',
message: 'Під час видалення сталася помилка.',
}
}
}Директива 'use server' повідомляє Next.js, що функції у цьому файлі повинні виконуватися на сервері.
Дані з FormData не можна вважати безпечними. Користувач може вручну змінити приховане поле форми або надіслати власний HTTP-запит.
Тому Server Action повинна:
перевірити, що поле id існує;
перевірити тип і формат значення;
переконатися, що запис існує;
перевірити права поточного користувача на його видалення.
У прикладі використано deleteMany(). Він повертає кількість видалених записів у властивості count. Це дозволяє обробити ситуацію, коли запис уже був видалений або не існував.
Тепер створимо клієнтський компонент app/posts/delete-post-form.tsx:
'use client'
import { useActionState } from 'react'
import { useFormStatus } from 'react-dom'
import type {
DeletePostState,
} from './actions'
type DeletePostAction = (
previousState: DeletePostState,
formData: FormData
) => Promise<DeletePostState>
type DeletePostFormProps = {
postId: string
action: DeletePostAction
}
const initialState: DeletePostState = {
status: 'idle',
message: '',
}
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? 'Видалення...' : 'Видалити'}
</button>
)
}
export function DeletePostForm({
postId,
action,
}: DeletePostFormProps) {
const [state, formAction] = useActionState(
action,
initialState
)
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
const confirmed = window.confirm(
'Ви дійсно хочете видалити цей запис?'
)
if (!confirmed) {
event.preventDefault()
}
}
return (
<form action={formAction} onSubmit={handleSubmit}>
<input type="hidden" name="id" value={postId} />
<SubmitButton />
{state.message && (
<p
role="status"
aria-live="polite"
data-status={state.status}
>
{state.message}
</p>
)}
</form>
)
}useActionStateuseActionState пов’язує Server Action зі станом форми:
const [state, formAction] = useActionState(
action,
initialState
)Значення:
state — останній результат виконання Server Action;
formAction — функція, яку потрібно передати у властивість action форми;
initialState — початковий стан до першого надсилання форми.
Server Action отримує два аргументи:
async function action(previousState, formData) {
// ...
}Перший аргумент — попередній стан, другий — дані форми.
useFormStatusКомпонент SubmitButton використовує useFormStatus() для отримання стану поточного надсилання форми.
Поки Server Action виконується:
кнопка блокується через disabled;
текст кнопки змінюється на Видалення...;
користувач не може випадково відправити форму кілька разів.
useFormStatus() повинен використовуватися у дочірньому компоненті форми. Викликати його безпосередньо в компоненті, який містить <form>, не слід.
Створимо сторінку app/posts/page.tsx:
import { db } from '@/lib/db'
import { deletePost } from './actions'
import { DeletePostForm } from './delete-post-form'
export default async function PostsPage() {
const posts = await db.post.findMany({
orderBy: {
createdAt: 'desc',
},
})
return (
<main>
<h1>Публікації</h1>
{posts.length === 0 ? (
<p>Публікацій поки немає.</p>
) : (
<ul>
{posts.map((post) => (
<li key={post.id}>
<h2>{post.title}</h2>
<DeletePostForm
postId={post.id}
action={deletePost}
/>
</li>
))}
</ul>
)}
</main>
)
}Сторінка є серверним компонентом за замовчуванням. Вона отримує список публікацій із бази даних і передає Server Action у клієнтський компонент форми.
Next.js дозволяє передавати Server Action у клієнтський компонент як властивість. При цьому сама функція залишається серверною — браузер не отримує її реалізацію.
Після успішного видалення викликається:
revalidatePath('/posts')Це повідомляє Next.js, що кеш сторінки /posts потрібно оновити. Після завершення Server Action сторінка повторно отримає дані, і видалений запис зникне зі списку.
Якщо не викликати revalidatePath, користувач може тимчасово бачити застарілий список, навіть якщо запис уже видалено з бази даних.
Server Action повертає об’єкт однакової структури для успішного та неуспішного завершення:
{
status: 'success',
message: 'Запис успішно видалено.'
}або:
{
status: 'error',
message: 'Запис не знайдено або його вже було видалено.'
}Завдяки цьому компонент форми може показати користувачу результат без додаткового API-маршруту.
Підтвердження виконується в обробнику onSubmit:
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
const confirmed = window.confirm(
'Ви дійсно хочете видалити цей запис?'
)
if (!confirmed) {
event.preventDefault()
}
}Якщо користувач натискає «Скасувати», викликається event.preventDefault(), тому форма не надсилається, а Server Action не запускається.
Важливо, що window.confirm() доступний тільки в браузері. Саме тому компонент із підтвердженням має директиву:
'use client'Не можна викликати window.confirm() усередині Server Action, оскільки Server Action виконується на сервері, де об’єкта window немає.
У Server Action потрібно обробляти щонайменше такі ситуації:
ідентифікатор не передано;
ідентифікатор має неправильний формат;
запис не існує;
запис уже видалив інший користувач;
база даних повернула помилку;
поточний користувач не має права на видалення.
Користувачу краще повертати загальне повідомлення:
return {
status: 'error',
message: 'Під час видалення сталася помилка.',
}Не варто показувати у браузері деталі помилки бази даних, SQL-запит або внутрішній стек викликів.
Приховане поле:
<input type="hidden" name="id" value={postId} />не захищає запис. Його значення можна змінити в інструментах розробника.
Перевірки повинні виконуватися в Server Action.
Перевірки існування запису недостатньо. Користувач може знати ідентифікатор чужого запису.
Перед видаленням потрібно перевірити, чи належить запис поточному користувачу або чи має він відповідну роль.
window.confirm() у серверному кодіServer Action не має доступу до браузерного API:
// Неправильно для Server Action
const confirmed = window.confirm('Видалити запис?')Підтвердження повинно виконуватися у клієнтському компоненті, а серверна функція повинна повторно перевірити всі важливі умови.
Без useFormStatus() користувач може натиснути кнопку кілька разів до завершення першого запиту. Це може призвести до зайвих запитів і неочікуваних повідомлень про помилку.
revalidatePathЯкщо після зміни даних не оновити кеш маршруту, список може залишатися застарілим.
Для useActionState Server Action повинна мати такий порядок аргументів:
async function action(previousState, formData) {
// ...
}Якщо спочатку очікувати FormData, значення будуть оброблятися неправильно.
Видалення можна реалізувати через форму, яка викликає Server Action.
window.confirm() використовується в клієнтському компоненті для підтвердження дії.
Server Action повинна повторно перевіряти ідентифікатор, існування запису та права доступу.
useActionState дозволяє отримати результат виконання Server Action у формі.
useFormStatus допомагає показати стан виконання та заблокувати кнопку.
revalidatePath() оновлює кеш сторінки після успішного видалення.
Повідомлення про помилки мають бути зрозумілими користувачу та не розкривати внутрішні деталі сервера.