Server Actions и мутации
Server Actions — функции, которые выполняются на сервере, но вызываются из клиента. Заменяют API-роуты для мутаций.
Определение
// app/actions.ts
'use server';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
await db.post.create({ data: { title } });
}
'use server' наверху файла помечает все экспорты как Server Actions.
В форме
import { createPost } from './actions';
export default function Form() {
return (
<form action={createPost}>
<input name="title" />
<button>Создать</button>
</form>
);
}
action принимает Server Action. Работает без JavaScript.
С аргументами
<form action={createPost}>
<input name="title" />
</form>
FormData передаётся автоматически.
Или через bind:
const createWithUserId = createPost.bind(null, userId);
<form action={createWithUserId}>...</form>
useActionState
Состояние и ошибки:
'use client';
import { useActionState } from 'react';
import { createPost } from './actions';
export default function Form() {
const [state, formAction, isPending] = useActionState(createPost, null);
return (
<form action={formAction}>
<input name="title" />
{state?.error && <p>{state.error}</p>}
<button disabled={isPending}>
{isPending ? 'Отправка...' : 'Создать'}
</button>
</form>
);
}
Action возвращает { error: string } или { success: true }.
Валидация
'use server';
export async function createPost(prevState: any, formData: FormData) {
const title = formData.get('title') as string;
if (!title || title.length < 3) {
return { error: 'Заголовок слишком короткий' };
}
await db.post.create({ data: { title } });
revalidatePath('/posts');
return { success: true };
}
revalidatePath
Обновить данные на странице:
import { revalidatePath } from 'next/cache';
await db.post.create({ data });
revalidatePath('/posts');
revalidateTag
Если fetch был с тегом:
revalidateTag('posts');
redirect
import { redirect } from 'next/navigation';
export async function createPost(formData: FormData) {
const post = await db.post.create({ data: { title: formData.get('title') } });
redirect(`/posts/${post.id}`);
}
Server Actions в обработчиках
'use client';
import { deletePost } from './actions';
export default function DeleteButton({ id }: { id: number }) {
return (
<button onClick={() => deletePost(id)}>
Удалить
</button>
);
}
Action может быть вызвана и напрямую.
С типами
'use server';
type ActionResult = { error?: string; success?: boolean };
export async function createPost(
prevState: ActionResult | null,
formData: FormData
): Promise<ActionResult> {
// ...
}
Безопасность
Server Actions — публичные HTTP-эндпоинты. Всегда проверяй:
- Авторизацию
- Права доступа
- Валидацию
'use server';
export async function deletePost(id: number) {
const user = await getCurrentUser();
if (!user) throw new Error('Unauthorized');
const post = await db.post.findUnique({ where: { id } });
if (post.authorId !== user.id) throw new Error('Forbidden');
await db.post.delete({ where: { id } });
}
Итоги
- Server Actions — мутации без API
'use server'наверху файлаaction={fn}в формеuseActionStateдля состоянияrevalidatePathиrevalidateTag- Всегда проверяй права