Next.js 16 Server Actions in 2026: mutaties, revalidatie en sollicitatievragen

Hoe Next.js 16 Server Actions omgaan met mutaties, revalidatie, pending-state, optimistische UI en beveiliging, met de sollicitatievragen die elk concept toetsen.

Stroomdiagram van Next.js Server Actions mutaties en revalidatie

Next.js Server Actions veranderen een gewone async-functie in een server-side mutatie-endpoint dat een formulier rechtstreeks kan aanroepen, zonder API-route, zonder client-fetch en zonder handmatige JSON-serialisatie. In Next.js 16 zijn ze de standaardmanier om datamutaties te schrijven en ze duiken op in vrijwel elk senior React-sollicitatiegesprek in 2026. Deze verdieping ontleedt hoe ze uitvoeren, hoe revalidatie verse data naar de UI doorgeeft, hoe pending- en foutstatussen te modelleren zijn en welke beveiligingsregels de meeste ontwikkelaars laten struikelen.

Definitie in één zin

Een Server Action is een async-functie gemarkeerd met "use server" die uitsluitend op de server draait en vanuit een client-component of een formulier kan worden aangeroepen alsof ze lokaal is, terwijl Next.js de netwerkaanvraag, argumentserialisatie en CSRF-bescherming automatisch afhandelt.

Hoe Server Actions onder de motorkap uitvoeren

Een Server Action is een uitsluitend server-side functie die via het netwerk wordt aangeroepen door een door de compiler gegenereerd endpoint. Wanneer een functie de "use server"-directive draagt, verstuurt de Next.js-compiler de body ervan nooit naar de browser. In plaats daarvan vervangt hij de functie door een lichtgewicht verwijzing: een gehasht action-ID dat wordt gekoppeld aan een gegenereerde POST-route. Het aanroepen van de action vanaf de client stuurt een aanvraag naar die route, de echte functie draait op de server en het geserialiseerde resultaat stroomt terug naar de aanroeper.

Dit ontwerp heeft drie gevolgen die het onthouden waard zijn, zowel voor productiecode als voor sollicitatiegesprekken. Ten eerste moeten argumenten en retourwaarden serialiseerbaar zijn, omdat ze als gecodeerde payloads het netwerk oversteken. Ten tweede blijven secrets op de server, aangezien de functie-body nooit voor de client wordt gebundeld. Ten derde werken actions die aan een <form> gekoppeld zijn zelfs vóór hydratie, en dat is precies waarom progressive enhancement een kernargument is. De officiële Next.js-gids over het bijwerken van data documenteert het volledige uitvoeringsmodel.

Elke Server Action leeft in een bestand of blok dat met "use server" is gemarkeerd. Eén enkele mutatie ziet er zo uit:

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}`)
}

De functie leest de ingediende FormData, slaat op via een service, invalideert het gecachte overzicht en leidt door. Er is nergens client-JavaScript voor nodig.

Een Server Action aan een formulier koppelen zonder client-JavaScript

De action doorgeven aan de action-prop van een formulier is de eenvoudigste integratie. Next.js serialiseert de indiening naar FormData en roept de functie op de server aan. Omdat dit het native indieningsmechanisme van formulieren gebruikt, functioneert het met uitgeschakelde JavaScript en schakelt het naadloos op zodra de pagina hydrateert.

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>
  )
}

Het name-attribuut op elk veld wordt de sleutel die formData.get() uitleest. De MDN-referentie voor FormData legt uit hoe de browser dit object uit het formulier samenstelt. Voor rijkere controle breidt next/form het native element uit met client-side navigatie en prefetching, terwijl hetzelfde action-gebaseerde contract behouden blijft.

Pending- en foutstatus beheren met useActionState

Echte formulieren hebben validatiefeedback en een laadindicator nodig. De React 19-hook useActionState verpakt een Server Action, rijgt een state-waarde door elke indiening heen en stelt een isPending-vlag beschikbaar. Hij vervangt de oudere naam useFormState en retourneert een tuple met de huidige state, een verpakte action en de pending-boolean.

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>
  )
}

Wanneer een hook de action aanstuurt, krijgt de functiesignatuur een eerste parameter prevState vóór FormData. Een object retourneren in plaats van gooien laat de component inline validatie renderen zonder 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: "" }
}

De useActionState-referentie op react.dev behandelt geavanceerde patronen zoals permalink-ondersteuning voor indieningen vóór hydratie. Gestructureerde state retourneren houdt het happy path en het foutpad op één voorspelbare plek.

revalidatePath vs revalidateTag: de juiste invalidatie kiezen

Een mutatie is maar het halve werk; de UI moet die weerspiegelen. Next.js stelt twee invalidatiefuncties beschikbaar vanuit next/cache, en de juiste kiezen is een veelvoorkomend onderscheid in sollicitatiegesprekken.

| Functie | Invalideert | Best wanneer | |----------|-------------|-----------| | revalidatePath("/posts") | Elke gecachte vermelding voor een route | De mutatie raakt één duidelijke pagina of layout | | revalidateTag("posts") | Elke gecachte fetch die met die tag is gelabeld | Dezelfde data verschijnt op meerdere routes |

revalidatePath is grof en route-gericht: het wist de cache voor een URL en de bijbehorende layout-boom. revalidateTag is fijnmazig en data-gericht: elke fetch of gecachte functie die met de string is getagd, wordt overal waar hij voorkomt geleegd. Tags schalen beter in grote apps, omdat één mutatie een zijbalk, een overzicht en een detailpagina in één aanroep kan verversen. Beide functies markeren data als verouderd in plaats van meteen opnieuw op te halen, dus de volgende aanvraag regenereert de inhoud.

Met Next.js 16 Cache Components integreert tagging met de "use cache"-directive via cacheTag, wat verandert hoe er in de hele app over caching wordt geredeneerd. Die interactie komt uitgebreid aan bod in de gids over Next.js 16 Cache Components op SharpSkill.

Klaar om je React / Next.js gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Optimistische UI-updates met useOptimistic

Netwerk-round-trips voegen merkbare latentie toe. De useOptimistic-hook rendert het verwachte resultaat direct en verzoent dat vervolgens met het serverantwoord zodra de action wordt afgehandeld. Hij neemt de huidige state en een reducer die de optimistische versie produceert.

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>
  )
}

Als de action mislukt, zet React de optimistische waarde automatisch terug naar de bevestigde serverstate, dus handmatige rollback is niet nodig. De useOptimistic-referentie op react.dev beschrijft hoe de verzoeningscyclus samenwerkt met transitions. Dit patroon zorgt ervoor dat formulieren met Server Actions net zo responsief aanvoelen als een volledig client-gerenderde SPA.

Argumenten binden en actions aanroepen vanuit event-handlers

Niet elke mutatie ontstaat uit een formulierindiening. Een rij verwijderen, een instelling omschakelen of een lijst herordenen wordt vaak getriggerd door een klik op een knop of een ander event. Een Server Action is op de client een gewone functieverwijzing, dus ze kan vanuit elke handler worden aangeroepen, mits de aanroep binnen een transition draait om de UI responsief te houden.

Extra argumenten die geen formuliervelden zijn, worden met bind toegevoegd, wat een nieuwe action oplevert met die waarden vooraan geplaatst. Dit is de idiomatische manier om een ID mee te geven naast de ingediende 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>
  )
}

Gebonden argumenten worden versleuteld zoals elke andere closed-over waarde, dus de postId wordt onderweg niet gemanipuleerd, hoewel de action nog steeds moet bevestigen dat de aanroeper op dat record mag handelen. De directe aanroep in startTransition verpakken laat React de interface interactief houden en brengt de pending-state naar voren zonder formulier-wrapper.

Beveiliging van Server Actions: behandel elke action als een publieke API

De gevaarlijkste misvatting is dat een Server Action privé is omdat ze naast servercode geschreven staat. In werkelijkheid genereert Next.js, zodra een action wordt gebruikt, een publiek POST-endpoint dat iedereen met een geprepareerde aanvraag kan aanroepen. Het framework levert veilige action-ID's en verwijdert ongebruikte actions tijdens next build, maar het autoriseert de aanroeper niet.

Autoriseer binnen elke action

Server Actions zijn bereikbare HTTP-endpoints. Sessiecontroles in een layout of middleware beschermen ze niet, omdat een aanvaller rechtstreeks naar de action kan POST'en. Verifieer de sessie en de rechten van de aanroeper binnen de action-body voordat er ook maar data wordt aangeraakt.

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)
}

Nog twee regels zijn van belang. Closed-over waarden die door een inline action worden vastgelegd, worden versleuteld voordat ze naar de client worden verzonden en op de server ontsleuteld, dus een vastgelegde token lekt niet in platte tekst, maar daarop vertrouwen is broos: houd secrets volledig buiten closures. En elk argument dat van de client komt, is niet-vertrouwde invoer die gevalideerd moet worden, precies zoals een REST-body. De Next.js-gids over databeveiliging formaliseert het taint-model achter deze garanties. Voor gestructureerde oefening op deze afwegingen drilt de interviewmodule over Next.js Server Actions op SharpSkill precies de vragen die aanwervingspanels stellen.

Sollicitatievragen over Next.js 16 Server Actions

Deze vragen duiken herhaaldelijk op in senior React- en full-stack-rondes in 2026.

Waarom kan een Server Action alleen serialiseerbare waarden accepteren en retourneren? Omdat argumenten en resultaten de netwerkgrens tussen client en server oversteken als gecodeerde payloads. Functies, class-instanties en DOM-nodes kunnen niet worden geserialiseerd, dus het doorgeven ervan gooit een fout. FormData, gewone objecten, arrays en primitieven zijn veilig.

Hoe werkt progressive enhancement met Server Actions? Wanneer een action rechtstreeks aan de action-prop van een formulier wordt doorgegeven, dient de browser het formulier native in via een POST-aanvraag, zelfs voordat React hydrateert. Next.js onderschept en draait de action server-side, dus het formulier is functioneel zonder client-JavaScript en schakelt na hydratie op naar een client-gestuurde ervaring.

Wat is het verschil tussen een fout gooien en een fout retourneren in een action? Gooien propageert naar de dichtstbijzijnde error boundary en is geschikt voor onverwachte fouten. Een gestructureerd state-object retourneren via useActionState is geschikt voor verwachte validatiefouten die inline moeten renderen zonder het formulier te unmounten.

Haalt het aanroepen van revalidatePath meteen opnieuw data op? Nee. Het markeert de gecachte vermeldingen als verouderd, zodat de volgende aanvraag ze regenereert. Het huidige antwoord blijft onaangetast, en daarom komt een redirect na een mutatie op vers gerenderde inhoud uit.

Voor de bredere renderingcontext waarbinnen deze mutaties draaien, sluit de analyse React 19 Server Components in productie natuurlijk aan bij kennis van Server Actions.

Klaar om je React / Next.js gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Conclusie

Next.js 16 Server Actions bundelen mutaties, revalidatie en progressive enhancement in één server-first primitief. De belangrijkste punten:

  • Markeer mutaties met "use server" en geef ze door aan de action-prop van een formulier om netwerkafhandeling en progressive enhancement gratis te krijgen.
  • Stuur pending- en validatiestate aan met useActionState en retourneer een gestructureerd object voor verwachte fouten in plaats van te gooien.
  • Grijp naar revalidatePath wanneer één route verandert en naar revalidateTag wanneer dezelfde data zich over meerdere routes uitstrekt.
  • Gebruik useOptimistic om het verwachte resultaat direct te renderen en laat React automatisch verzoenen of terugdraaien.
  • Autoriseer en valideer binnen elke action-body, omdat elke gebruikte action een publiek HTTP-endpoint is dat middleware alleen niet beschermt.

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Tags

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

Delen

Gerelateerde artikelen