Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Дії сервера в Next.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Server Actions** - це async-функції з директивою `'use server'`, які виконуються на сервері і дозволяють змінювати дані прямо з React-компонентів, без окремого API-маршруту. ```tsx 'use server' export async function updateUser(formData: FormData) { await db.user.update({ where: { id }, data: { name: formData.get('name') } }) revalidatePath('/settings') } ``` **Ключове:** Server Actions - для мутацій з UI. Для публічних API і вебхуків використовуй Route Handlers.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Server Actions** - це async-функції з директивою `'use server'`, які виконуються на сервері і дозволяють змінювати дані прямо з React-компонентів, без окремого API-ендпоінта. ## Теорія ### TL;DR - Позначаєш функцію `'use server'` - вона запускається на сервері, браузер її не бачить - Передаєш в `<form action={...}>` - форма працює навіть без JavaScript на сторінці - Викликаєш з Client Component через `useTransition` - отримуєш стан завантаження - Валідація обов'язкова: функція публічно доступна як будь-який HTTP-ендпоінт - Після запису - `revalidatePath` або `revalidateTag`, щоб прибрати застарілі кешовані дані ### Швидкий приклад ```tsx // actions/user.ts 'use server' import { db } from '@/lib/db' import { revalidatePath } from 'next/cache' export async function updateUserName(formData: FormData) { const name = formData.get('name') as string await db.user.update({ where: { id: currentUserId }, data: { name } }) revalidatePath('/settings') // кажемо Next.js перемалювати цей маршрут } ``` Next.js автоматично створює HTTP POST-ендпоінт для цієї функції. При сабміті форми браузер звертається до нього напряму - жодного клієнтського fetch не потрібно. ### Чим Server Actions відрізняються від Route Handlers До Server Actions мутація даних через форму вимагала трьох файлів: Server Component для отримання даних, Client Component для стану і Route Handler для POST-запиту. Server Actions замінюють усі три однією функцією. Але є межа: вони призначені тільки для мутацій з UI. Публічні API, вебхуки й інтеграції зі сторонніми сервісами - це завдання для Route Handlers. URL Server Action генерується автоматично і може змінитись між білдами, тому зовнішні сервіси не можуть покладатися на нього. ### Як працює директива `'use server'` можна поставити на рівні файлу (тоді кожен експорт стає Server Action) або всередині окремої функції: ```tsx // Рівень файлу: всі функції у цьому файлі виконуються на сервері 'use server' export async function createPost(data: PostData) { ... } export async function deletePost(id: string) { ... } ``` ```tsx // Рівень функції: тільки ця функція виконується на сервері export default async function Page() { async function handleSubmit(formData: FormData) { 'use server' // виконується на сервері } return <form action={handleSubmit}>...</form> } ``` Рівень файлу - стандартний підхід у великих проектах. Рівень функції зручний для разових дій всередині Server Component. ### Використання з формами Передаєш action напряму в `<form action={...}>`. React перехоплює сабміт і викликає функцію. Якщо JavaScript ще не завантажився, браузер відправить форму як звичайний POST - action все одно спрацює. Прогресивне покращення без жодного додаткового коду: ```tsx // app/settings/page.tsx import { updateUserName } from '@/actions/user' export default async function SettingsPage() { return ( <form action={updateUserName}> <input name="name" placeholder="Ім'я на IT Lead" /> <button type="submit">Зберегти</button> </form> ) } ``` ### Використання з useTransition Форми - один сценарій. Але іноді мутацію потрібно викликати з кнопки, не через сабміт. Загортаєш виклик у `startTransition`: ```tsx 'use client' import { useTransition } from 'react' import { solveProblem } from '@/actions/problem' export function SolveButton({ problemId, userId }: { problemId: string userId: string }) { const [isPending, startTransition] = useTransition() return ( <button disabled={isPending} onClick={() => startTransition(() => solveProblem(problemId, userId))} > {isPending ? 'Перевірка...' : 'Вирішено'} </button> ) } ``` `isPending` стає `true` поки action виконується. Використовуй для блокування кнопки або показу спінера. У React 19 з'явився `useActionState` - він дає зручніший API, коли потрібно ще й читати значення, яке повертає action. ### Валідація даних Server Action доступний як HTTP-ендпоінт. Ставитись до вхідних даних треба так само, як до будь-якого зовнішнього запиту: ```tsx 'use server' import { z } from 'zod' import { db } from '@/lib/db' import { revalidatePath } from 'next/cache' const schema = z.object({ name: z.string().min(2).max(50), email: z.string().email() }) export async function updateUser(formData: FormData) { const result = schema.safeParse({ name: formData.get('name'), email: formData.get('email') }) if (!result.success) { return { error: result.error.flatten().fieldErrors } } await db.user.update({ where: { id: currentUserId }, data: result.data }) revalidatePath('/settings') return { success: true } } ``` Повертай об'єкт з `error` або `success` замість того, щоб кидати виняток. Клієнтський компонент зможе прочитати це значення і показати помилки на рівні окремих полів. ### Інвалідація кешу після запису Next.js кешує агресивно. Після зміни даних потрібно позначити, що кеш застарів: ```tsx 'use server' import { revalidatePath, revalidateTag } from 'next/cache' import { redirect } from 'next/navigation' export async function createProblem(data: ProblemData) { await db.problem.create({ data }) revalidateTag('problems') // очищує записи кешу з тегом 'problems' revalidatePath('/problems') // очищує Full Route Cache для цього шляху redirect('/problems') // перенаправляє після створення } ``` `revalidateTag` точніший: він очищує тільки ті fetch-запити, що були позначені `{ next: { tags: ['problems'] } }`. `revalidatePath` прибирає все, що кешовано для цього URL. Якщо кілька маршрутів використовують одні дані - `revalidateTag` доречніший. ### Типові помилки **1. Відсутність валідації** ```tsx // Неправильно: довіряємо сирому FormData export async function updateUser(formData: FormData) { await db.user.update({ data: { name: formData.get('name') } }) } // Правильно: спочатку валідуємо const result = schema.safeParse({ name: formData.get('name') }) if (!result.success) return { error: result.error.flatten() } ``` **2. `'use server'` у файлі Client Component** Server Actions не можна оголошувати всередині файлу з `'use client'`. Оголошуй їх в окремому файлі або в Server Component, а потім імпортуй у Client Component. **3. Забули викликати revalidate** Мутація пройшла, але UI показує старі дані. За моїм досвідом це найпоширеніший баг, який трапляється з командами, що тільки починають використовувати Server Actions. Після кожного запису - `revalidatePath` або `revalidateTag`. Якщо використовуєш `redirect`, Next.js автоматично інвалідує цільовий маршрут. **4. Server Actions замість публічного API** URL Server Action генерується при білді і не є стабільним між деплоями. Зовнішні сервіси не можуть звертатися до нього надійно. Для вебхуків і зовнішніх інтеграцій - Route Handlers. **5. Виняток замість повернення помилки** ```tsx // Ризиковано: action кидає виняток, компонент потрапляє в Error Boundary await updateUser(formData) // Краще: повертаємо помилку об'єктом const result = await updateUser(formData) if (!result.success) setErrors(result.error) ``` ### Де використовуються на практиці - Форми налаштувань профілю: ім'я, аватар, email - Позначення виконаних елементів: вирішені задачі, прочитані нотифікації - Створення записів: пост, коментар, задача - Auth-флоу: вхід, вихід, реєстрація разом з `redirect` - Оптимістичний UI: разом з `useOptimistic` (React 19) для миттєвого відгуку до відповіді сервера ### Питання на співбесіді **Q:** Що станеться, якщо Server Action кине необроблений виняток? **A:** Помилка потрапить до найближчого Error Boundary, якщо action викликається з Client Component. При виклику напряму з форми сторінка перемалюється без краша. Повертати об'єкт з помилкою надійніше, ніж кидати виняток. **Q:** Чи може Server Action перенаправити користувача? **A:** Так. Виклик `redirect('/path')` з `next/navigation` працює так само, як у Server Component - всередині він кидає спеціальний виняток, який Next.js перехоплює і обробляє. **Q:** Яка різниця між Server Action і Route Handler? **A:** Server Actions прив'язані до дерева React-компонентів, приймають `FormData` і викликаються з UI. Route Handlers - стандартні HTTP-ендпоінти для будь-яких клієнтів: мобільних застосунків, сторонніх сервісів, cron-завдань. Викликати Server Action з мобільного застосунку ненадійно. **Q:** Як Next.js захищає Server Actions від CSRF? **A:** Next.js генерує випадковий ID дії під час білду і перевіряє його при кожному POST. Це базовий захист від CSRF за замовчуванням. Авторизацію - хто має право викликати конкретну функцію - перевіряти треба самостійно всередині action. **Q:** Чи можна викликати Server Action з іншого Server Action? **A:** Так, але це звичайний async-виклик без HTTP. Просто імпортуєш і викликаєш. Директива `'use server'` створює HTTP-ендпоінт тільки для викликів з клієнта. ## Приклади ### Форма профілю з валідацією і показом помилок ```tsx // actions/profile.ts 'use server' import { z } from 'zod' import { db } from '@/lib/db' import { revalidatePath } from 'next/cache' const profileSchema = z.object({ name: z.string().min(2).max(50), }) export async function updateProfile( prevState: unknown, formData: FormData ) { const result = profileSchema.safeParse({ name: formData.get('name') }) if (!result.success) { return { error: result.error.flatten().fieldErrors } } await db.user.update({ where: { id: currentUserId }, data: result.data, }) revalidatePath('/settings') return { success: true } } ``` ```tsx // app/settings/page.tsx 'use client' import { useActionState } from 'react' import { updateProfile } from '@/actions/profile' export default function SettingsPage() { const [state, action] = useActionState(updateProfile, null) return ( <form action={action}> <input name="name" /> {state?.error?.name && <p>{state.error.name[0]}</p>} <button type="submit">Зберегти</button> </form> ) } ``` `useActionState` пов'язує значення, яке повертає action, зі станом компонента. Параметр `prevState` отримує попереднє значення при кожному виклику - зручно для накопичення помилок або підрахунку спроб. ### Оптимістичне оновлення з useOptimistic ```tsx 'use client' import { useOptimistic, useTransition } from 'react' import { toggleSolved } from '@/actions/problem' export function ProblemList({ problems }: { problems: Problem[] }) { const [optimisticProblems, addOptimistic] = useOptimistic( problems, (state, solvedId: string) => state.map(p => (p.id === solvedId ? { ...p, solved: true } : p)) ) const [, startTransition] = useTransition() function handleSolve(problemId: string) { startTransition(async () => { addOptimistic(problemId) // UI оновлюється миттєво await toggleSolved(problemId) // зберігаємо в БД }) } return ( <ul> {optimisticProblems.map(p => ( <li key={p.id}> {p.title} <button onClick={() => handleSolve(p.id)} disabled={p.solved}> {p.solved ? 'Виконано' : 'Позначити виконаним'} </button> </li> ))} </ul> ) } ``` Список оновлюється до того, як сервер відповів. Якщо action завершиться помилкою, React автоматично відкатить оптимістичний стан. ### Auth-флоу з redirect ```tsx // actions/auth.ts 'use server' import { cookies } from 'next/headers' import { redirect } from 'next/navigation' import { z } from 'zod' import { verifyCredentials, createSession } from '@/lib/auth' const loginSchema = z.object({ email: z.string().email(), password: z.string().min(8), }) export async function login(formData: FormData) { const result = loginSchema.safeParse({ email: formData.get('email'), password: formData.get('password'), }) if (!result.success) { return { error: 'Невірні дані' } } const user = await verifyCredentials(result.data) if (!user) { return { error: 'Неправильний email або пароль' } } const session = await createSession(user.id) cookies().set('session', session.token, { httpOnly: true, secure: true }) redirect('/dashboard') } ``` Валідація, перевірка облікових даних, httpOnly-кука і перенаправлення - все в одній функції, без Route Handler.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.