Server Actions у Next.js 16 у 2026: мутації, ревалідація та питання співбесід

Як Server Actions у Next.js 16 обробляють мутації, ревалідацію, стан очікування, оптимістичний інтерфейс і безпеку, разом із питаннями співбесід на кожну концепцію.

Схема потоку мутацій і ревалідації Server Actions у Next.js

Server Actions у Next.js перетворюють звичайну асинхронну функцію на серверний ендпоінт для мутацій, який форма може викликати напряму — без API-маршруту, без клієнтського fetch і без ручної серіалізації JSON. У Next.js 16 це стандартний спосіб писати мутації даних, і вони зустрічаються майже на кожній співбесіді для senior-розробників React у 2026 році. Цей детальний розбір пояснює, як вони виконуються, як ревалідація доносить свіжі дані до інтерфейсу, як моделювати стани очікування й помилок, а також правила безпеки, на яких спотикається більшість розробників.

Визначення в одному реченні

Server Action — це асинхронна функція, позначена директивою "use server", яка виконується лише на сервері й може викликатися з клієнтського компонента або форми так, ніби вона локальна, тоді як Next.js автоматично бере на себе мережевий запит, серіалізацію аргументів і захист від CSRF.

Як Server Actions виконуються під капотом

Server Action — це функція, що виконується виключно на сервері та викликається через мережу за допомогою ендпоінта, згенерованого компілятором. Коли функція має директиву "use server", компілятор Next.js ніколи не надсилає її тіло до браузера. Натомість він замінює функцію на легке посилання: хешований ідентифікатор дії, прив'язаний до згенерованого POST-маршруту. Виклик дії з клієнта надсилає запит на цей маршрут, справжня функція виконується на сервері, а серіалізований результат стрімиться назад до того, хто її викликав.

Такий дизайн має три наслідки, які варто пам'ятати як для продакшн-коду, так і для співбесід. По-перше, аргументи та значення, що повертаються, мають бути серіалізовними, бо вони перетинають мережу у вигляді закодованих даних. По-друге, секрети залишаються на сервері, оскільки тіло функції ніколи не потрапляє до клієнтського бандла. По-третє, дії, прив'язані до <form>, працюють навіть до гідратації, і саме тому progressive enhancement є ключовою перевагою. Офіційний посібник Next.js з оновлення даних документує повну модель виконання.

Кожна Server Action живе у файлі чи блоці, позначеному "use server". Одна мутація виглядає так:

app/posts/actions.tstypescript
"use server"

import { revalidatePath } from "next/cache"
import { redirect } from "next/navigation"
import { PostService } from "@/lib/services/post-service"

export async function createPost(formData: FormData) {
  // FormData values arrive as strings; cast explicitly
  const title = formData.get("title") as string
  const body = formData.get("body") as string

  // Run business logic through the service layer, never the ORM directly
  const post = await PostService.create({ title, body })

  // Purge the cached list so the next request refetches it
  revalidatePath("/posts")

  // Send the user to the freshly created resource
  redirect(`/posts/${post.id}`)
}

Функція читає надіслані FormData, зберігає дані через сервіс, інвалідовує кешований список і виконує перенаправлення. Жодного клієнтського JavaScript для цього не потрібно.

Підключення Server Action до форми без клієнтського JavaScript

Передача дії у пропс action форми — це найпростіша інтеграція. Next.js серіалізує надіслані дані у FormData і викликає функцію на сервері. Оскільки використовується нативний механізм надсилання форми, це працює навіть із вимкненим JavaScript і безшовно покращується після гідратації сторінки.

app/posts/new-post-form.tsxtsx
import { createPost } from "./actions"

export default function NewPostForm() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <textarea name="body" required />
      <button type="submit">Publish</button>
    </form>
  )
}

Атрибут name кожного поля стає ключем, який зчитує formData.get(). Довідник MDN щодо FormData пояснює, як браузер складає цей об'єкт із форми. Для гнучкішого керування next/form розширює нативний елемент клієнтською навігацією та передзавантаженням, зберігаючи той самий контракт на основі дій.

Керування станами очікування та помилок за допомогою useActionState

Справжнім формам потрібен зворотний зв'язок для валідації та індикатор завантаження. Хук useActionState із React 19 обгортає Server Action, проводить значення стану крізь кожне надсилання та надає прапорець isPending. Він замінює стару назву useFormState і повертає кортеж із поточного стану, обгорнутої дії та булевого значення очікування.

app/posts/new-post-form.tsxtsx
"use client"

import { useActionState } from "react"
import { createPost } from "./actions"

const initialState = { error: "" }

export default function NewPostForm() {
  // state carries the last return value; isPending tracks the in-flight request
  const [state, formAction, isPending] = useActionState(createPost, initialState)

  return (
    <form action={formAction}>
      <input name="title" required />
      <textarea name="body" required />
      {state.error ? <p role="alert">{state.error}</p> : null}
      <button type="submit" disabled={isPending}>
        {isPending ? "Publishing..." : "Publish"}
      </button>
    </form>
  )
}

Коли дією керує хук, сигнатура функції отримує перший параметр prevState перед FormData. Повернення об'єкта замість викидання винятку дозволяє компоненту показувати вбудовану валідацію без error boundary:

app/posts/actions.tstypescript
"use server"

import { revalidateTag } from "next/cache"
import { PostService } from "@/lib/services/post-service"

type FormState = { error: string }

export async function createPost(
  prevState: FormState,
  formData: FormData,
): Promise<FormState> {
  const title = formData.get("title") as string

  // Validate on the server; client validation is never trustworthy alone
  if (!title || title.length < 3) {
    return { error: "Title must be at least 3 characters." }
  }

  await PostService.create({ title })

  // Invalidate by tag rather than path for finer-grained control
  revalidateTag("posts")
  return { error: "" }
}

Довідник useActionState на react.dev охоплює складніші патерни, наприклад підтримку permalink для надсилань до гідратації. Повернення структурованого стану тримає успішний і помилковий сценарії в одному передбачуваному місці.

revalidatePath проти revalidateTag: вибір правильної інвалідації

Мутація — це лише половина справи; інтерфейс має її відобразити. Next.js надає дві функції інвалідації з next/cache, і вибір правильної — часте питання, що розрізняє кандидатів на співбесіді.

| Функція | Що інвалідовує | Коли використовувати | |----------|-------------|-----------| | revalidatePath("/posts") | Кожен кешований запис для маршруту | Мутація впливає на одну конкретну сторінку чи layout | | revalidateTag("posts") | Кожен кешований fetch із цим тегом | Ті самі дані з'являються на кількох маршрутах |

revalidatePath — грубий та орієнтований на маршрут: він очищає кеш для URL і його дерева layout. revalidateTag — точковий та орієнтований на дані: будь-який fetch чи кешована функція, позначені рядком-тегом, скидаються, де б вони не перебували. Теги краще масштабуються у великих застосунках, бо одна мутація може оновити бічну панель, список і сторінку деталей за один виклик. Обидві функції позначають дані як застарілі, а не перезавантажують їх одразу, тож наступний запит регенерує вміст.

У Cache Components із Next.js 16 теги інтегруються з директивою "use cache" через cacheTag, що змінює підхід до кешування в межах усього застосунку. Цю взаємодію детально розглянуто в посібнику з Cache Components у Next.js 16 на SharpSkill.

Готовий до співбесід з React / Next.js?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Оптимістичні оновлення інтерфейсу за допомогою useOptimistic

Мережеві звернення додають помітну затримку. Хук useOptimistic миттєво відображає очікуваний результат, а потім узгоджує його з відповіддю сервера після завершення дії. Він приймає поточний стан і редюсер, що створює оптимістичну версію.

app/posts/like-button.tsxtsx
"use client"

import { useOptimistic } from "react"
import { likePost } from "./actions"

export function LikeButton({ postId, likes }: { postId: string; likes: number }) {
  // optimisticLikes updates before the server confirms
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    likes,
    (current) => current + 1,
  )

  async function handleLike() {
    addOptimisticLike(null) // update the UI immediately
    await likePost(postId)  // then run the real mutation
  }

  return (
    <form action={handleLike}>
      <button type="submit">Like ({optimisticLikes})</button>
    </form>
  )
}

Якщо дія завершується невдало, React автоматично повертає оптимістичне значення до підтвердженого серверного стану, тож ручний відкат не потрібен. Довідник useOptimistic на react.dev детально описує, як цикл узгодження взаємодіє з transitions. Саме цей патерн робить форми на Server Actions такими ж чутливими, як повністю клієнтський SPA.

Прив'язка аргументів і виклик дій з обробників подій

Не кожна мутація починається з надсилання форми. Видалення рядка, перемикання налаштування чи зміна порядку списку часто спрацьовують від кліку по кнопці або іншої події. Server Action — це звичайне посилання на функцію на клієнті, тож її можна викликати з будь-якого обробника, за умови що виклик відбувається всередині transition, щоб інтерфейс залишався чутливим.

Додаткові аргументи, які не є полями форми, приєднуються за допомогою bind, що створює нову дію з цими значеннями на початку. Це ідіоматичний спосіб передати ідентифікатор разом із надісланими FormData.

app/posts/post-actions.tsxtsx
"use client"

import { useTransition } from "react"
import { deletePost, updatePost } from "./actions"

export function PostActions({ postId }: { postId: string }) {
  const [isPending, startTransition] = useTransition()

  // Bind the id so the action receives it before FormData
  const updateWithId = updatePost.bind(null, postId)

  return (
    <div>
      <form action={updateWithId}>
        <input name="title" />
        <button type="submit">Save</button>
      </form>

      {/* Event-handler invocation wrapped in a transition */}
      <button
        onClick={() => startTransition(() => deletePost(postId))}
        disabled={isPending}
      >
        {isPending ? "Deleting..." : "Delete"}
      </button>
    </div>
  )
}

Прив'язані аргументи шифруються, як і будь-яке інше замкнуте значення, тож postId не можна підмінити під час передавання, хоча дія все одно повинна перевірити, що той, хто її викликає, має право діяти з цим записом. Обгортання прямого виклику в startTransition дозволяє React тримати інтерфейс інтерактивним і показувати стан очікування без обгортання у форму.

Безпека Server Actions: кожна дія — це публічний API

Найнебезпечніша хибна думка полягає в тому, що Server Action є приватною, бо написана поряд із серверним кодом. Насправді, щойно дія використовується, Next.js генерує публічний POST-ендпоінт, який будь-хто може викликати сформованим запитом. Фреймворк надає безпечні ідентифікатори дій і видаляє невикористані дії під час next build, але він не авторизує того, хто викликає.

Авторизація всередині кожної дії

Server Actions — це доступні HTTP-ендпоінти. Перевірки сесії в layout чи middleware не захищають їх, бо зловмисник може надіслати POST безпосередньо до дії. Сесію та права того, хто викликає, слід перевіряти всередині тіла дії, перш ніж торкатися будь-яких даних.

app/admin/actions.tstypescript
"use server"

import { auth } from "@/lib/auth"
import { headers } from "next/headers"
import { UserService } from "@/lib/services/user-service"

export async function deleteUser(userId: string) {
  // Authenticate on every invocation, not in a wrapper component
  const session = await auth.api.getSession({ headers: await headers() })
  if (!session || session.user.role !== "admin") {
    throw new Error("Unauthorized")
  }

  await UserService.delete(userId)
}

Ще два правила мають значення. Замкнуті значення, захоплені вбудованою дією, шифруються перед надсиланням клієнту й розшифровуються на сервері, тож захоплений токен не витікає у відкритому вигляді, але покладатися на це ненадійно: секрети взагалі не варто тримати в замиканнях. І будь-який аргумент, що надходить від клієнта, є недовіреним вводом, який потрібно валідувати, точно як тіло REST-запиту. Посібник Next.js з безпеки даних формалізує модель taint, що стоїть за цими гарантіями. Для структурованої практики цих компромісів модуль співбесід із Server Actions у Next.js на SharpSkill відпрацьовує саме ті запитання, які ставлять комісії з найму.

Питання для співбесіди щодо Server Actions у Next.js 16

Ці питання регулярно зринають на співбесідах для senior-розробників React і full-stack у 2026 році.

Чому Server Action може приймати й повертати лише серіалізовні значення? Тому що аргументи й результати перетинають мережеву межу між клієнтом і сервером у вигляді закодованих даних. Функції, екземпляри класів і DOM-вузли не можна серіалізувати, тож їх передавання спричиняє помилку. FormData, звичайні об'єкти, масиви й примітиви — безпечні.

Як працює progressive enhancement із Server Actions? Коли дію передано напряму в пропс action форми, браузер надсилає форму нативно через POST-запит ще до гідратації React. Next.js перехоплює запит і виконує дію на сервері, тож форма працює без клієнтського JavaScript і покращується до клієнтського досвіду після гідратації.

Яка різниця між викиданням і поверненням помилки в дії? Викидання винятку поширюється до найближчого error boundary й підходить для неочікуваних збоїв. Повернення структурованого об'єкта стану через useActionState підходить для очікуваних помилок валідації, які мають відображатися вбудовано, не розмонтовуючи форму.

Чи викликає revalidatePath негайне перезавантаження даних? Ні. Він позначає кешовані записи як застарілі, щоб наступний запит їх регенерував. Поточна відповідь не змінюється, і саме тому перенаправлення після мутації потрапляє на щойно відрендерений вміст.

Для ширшого контексту рендерингу, у якому виконуються ці мутації, розбір React 19 Server Components у продакшні природно доповнює знання про Server Actions.

Готовий до співбесід з React / Next.js?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Висновок

Server Actions у Next.js 16 об'єднують мутації, ревалідацію та progressive enhancement в єдиний серверний примітив. Головні висновки:

  • Мутації варто позначати директивою "use server" й передавати у пропс action форми, щоб безкоштовно отримати обробку мережі та progressive enhancement.
  • Станами очікування й валідації керує useActionState, що повертає структурований об'єкт для очікуваних помилок замість викидання винятку.
  • revalidatePath підходить, коли змінюється один маршрут, а revalidateTag — коли ті самі дані охоплюють кілька маршрутів.
  • useOptimistic миттєво відображає очікуваний результат, а React автоматично узгоджує його або відкочує.
  • Авторизацію й валідацію слід виконувати всередині тіла кожної дії, бо кожна використана дія — це публічний HTTP-ендпоінт, який middleware сам по собі не захищає.

Починай практикувати!

Перевір свої знання з нашими симуляторами співбесід та технічними тестами.

Теги

#next.js
#server-actions
#react
#mutations
#revalidation
#interview

Поділитися

Пов'язані статті