Ga naar inhoud
Thomas Foundry
← LogboekTech tip

Waarom server actions je client JS halveren

Een patroon waar veel teams nog niet bij stilstaan. Een korte uitleg met code, en wanneer je het níet wil.

·4 min lezen

Het oude patroon

Drie jaar geleden zag een typische form-submit in een Next.js-app er zo uit:

// app/components/contact-form.tsx (oud)
"use client";
 
export function ContactForm() {
  async function submit(data: FormData) {
    const res = await fetch("/api/contact", {
      method: "POST",
      body: JSON.stringify({
        name: data.get("name"),
        email: data.get("email"),
      }),
      headers: { "Content-Type": "application/json" },
    });
    if (!res.ok) {
      // toast, error state, etc.
    }
  }
  return <form action={submit}>...</form>;
}

En daarnaast — in een aparte file — een API-route:

// app/api/contact/route.ts
import { z } from "zod";
 
const schema = z.object({ name: z.string(), email: z.string().email() });
 
export async function POST(req: Request) {
  const body = await req.json();
  const data = schema.parse(body);
  // ... do work
  return Response.json({ ok: true });
}

Twee bestanden. Twee zod-schemas die je in sync moet houden. Eén fetch call met de bekende footguns: handmatige headers, JSON-stringify, error-handling die je vaak vergeet. En — niet te onderschatten — een hele blob client JavaScript voor het serialiseren en verzenden van een formulier.

Wat server actions vervangen

In Next 14+ kun je dit hele blok in één bestand zetten:

// app/contact-form.tsx
import { z } from "zod";
 
const schema = z.object({ name: z.string(), email: z.string().email() });
 
async function submitContact(formData: FormData) {
  "use server";
  const data = schema.parse({
    name: formData.get("name"),
    email: formData.get("email"),
  });
  // ... do work directly here
  return { ok: true };
}
 
export function ContactForm() {
  return <form action={submitContact}>...</form>;
}

Geen "use client". Geen fetch. Geen API-route. Eén zod-schema. De form-submit zelf gebeurt via een progressively-enhanced HTTP-post naar dezelfde URL, met of zonder JavaScript.

Wat is er aan de hand?

Server actions worden bundled als een POST-endpoint dat Next zelf genereert, met een protocol-laag erbovenop voor argument-serialisatie. Voor de gebruiker betekent dat:

  1. De form werkt zonder JavaScript. Vóórdat de page-bundle gehydrateerd is, of in browsers waar JS uit staat, doet het formulier gewoon een POST en redirect terug.
  2. Geen client-side fetch-orchestratie. Geen optimistic update via TanStack Query, geen handmatige loading-state — useFormStatus doet dat in een paar regels.
  3. Het client-side JS-payload daalt. Voor een Lumello-achtige app met 12 formulieren per pagina was de winst ongeveer 40-55% minder client JS. Niet omdat de form-code minder werd, maar omdat het hele fetch/state/validatie-frame uit de client bundle viel.

Wanneer je het níet wil

Server actions zijn geen wondermiddel:

  • Bulk-bewerkingen met realtime feedback (denk: een Kanban-board waar je 30 cards tegelijk versleept) — voor die UI's wil je nog steeds een client-side store + optimistic updates. Server actions per drag-end zou je server platleggen.
  • Acties die buiten de request-context state nodig hebben (long polling, websockets) — server actions zijn fundamenteel POST-requests. Voor live-state heb je nog steeds een aparte websocket-laag nodig.
  • Third-party widgets met eigen state machines (Mapbox draws, Stripe Elements) — die zijn al client-only en je verliest niks door fetch-bij-hen-te-laten.

Conclusie

Server actions zijn niet "de nieuwe REST". Het zijn de juiste default voor 80% van form-submits en mutaties in een modern Next-project. De resterende 20% is waarom je nog steeds een API-laag op de plank houdt.

Voor Lumello en Sealr is alles vanuit het portaal — afspraken maken, deals updaten, NDA-handtekeningen — server-action-driven. Het verschil tegenover een REST-API-aanpak is niet alleen ontwikkelsnelheid; het is een meetbaar kleinere client-bundle die op slechtere netwerken sneller bruikbaar wordt.