Server Actions i Next.js: en formular, der virker, før JavaScript er indlæst
Server Actions i Next.js: en formular, der virker, før JavaScript er indlæst
Det klassiske formularmønster i React: onSubmit, event.preventDefault(), fetch('/api/todos', { method: 'POST', body: JSON.stringify(...) }), useState til at håndtere indlæsningstilstand. Det virker – på én betingelse, det er let at glemme, fordi den aldrig viser sig på din egen bærbare med hurtig forbindelse: JavaScript skal nå at blive indlæst, parset og eksekveret, før formularen overhovedet begynder at reagere på et klik. På en langsom mobilforbindelse, midt i hydreringen af en stor side, eller hvis scriptet bliver blokeret af en browserudvidelse – klikker brugeren på "Send", og der sker bogstaveligt talt ingenting. Ingen fejl, ingen spinner – onClick findes simpelthen endnu ikke.
Server Actions i Next.js bliver ofte fremstillet som en mere bekvem syntaks til præcis det samme kald – "fetch, bare uden at du skal skrive en API-rute". Det er en forsimpling, der overser det, der faktisk er nyt ved mekanismen: <form action={minAction}> bygger på browserens native, altid eksisterende mekanisme til at indsende formularer, ikke på JavaScript som en nødvendig forudsætning. Det ændrer både, hvordan det virker under motorhjelmen, og hvilke fejl teams i praksis begår, når de behandler "use server" som et almindeligt kald til en lokal funktion.
Hvad direktivet "use server" egentlig gør
Når du mærker en funktion med "use server", efterlader Next.js' kompiler ikke dens indhold i den pakke, browseren modtager. Den skærer det ud og erstatter det med en reference – en krypteret identifikator, der angiver, hvilken funktion på serveren der skal kaldes. Det, der faktisk havner hos klienten, er en lille stub: "når du kalder mig, send en POST til et specielt RSC-endepunkt med denne identifikator og de serialiserede argumenter". Selve logikken – en databaseforespørgsel, validering, hvad funktionen nu end gør – forlader aldrig fysisk serveren.
tsx
// app/actions.ts"use server";import { revalidatePath } from "next/cache";import { db } from "@/lib/db";export async function addTodoAction(prevState: unknown, formData: FormData) {const title = formData.get("title");if (typeof title !== "string" || title.trim().length === 0) {return { error: "Titlen må ikke være tom." };}await db.todo.create({ data: { title: title.trim() } });revalidatePath("/todos");return { error: null };}
Når du hæfter den funktion på <form action={addTodoAction}>, sker der noget, en almindelig fetch aldrig gav dig gratis: det virker, før nogen JavaScript overhovedet er eksekveret. Browseren har altid kunnet sende en formular som en native POST, hvis action-attributten peger på en URL – React 19 og Next.js udvider det, så action også kan være en serverfunktion, og browseren følger stadig sin native vej, hvis JS endnu ikke er klar. Når JavaScript allerede kører, opfanger Next.js hændelsen og gør det samme via fetch i baggrunden, uden at genindlæse siden, med en flydende opdatering af UI'et. Det er progressive enhancement indbygget i frameworket, ikke noget, du selv skal programmere – præcis den samme mekanisme, vi skrev om i forbindelse med RouteAnnouncer i artiklen om tilgængelighed, blot anvendt på datamutation i stedet for navigation.
Praktisk eksempel: en formular med fuld tilstandshåndtering, uden manuel fetch
React 19 tilføjede useActionState specifikt til dette mønster – den kobler kaldet af en serveraktion sammen med den tilstand, den senest returnerede, uden en manuel useState til at gemme fejl eller indlæsningsstatus.
tsx
"use client";import { useActionState } from "react";import { addTodoAction } from "./actions";const initialState = { error: null };const TodoForm = () => {const [state, formAction, isPending] = useActionState(addTodoAction,initialState,);return (<form action={formAction}><input type="text" name="title" placeholder="Ny opgave" required /><button type="submit" disabled={isPending}>{isPending ? "Tilføjer…" : "Tilføj"}</button>{state.error && <p className="form-error">{state.error}</p>}</form>);};export default TodoForm;
Flere ting sker her samtidig, uden en linje kode brugt på manuel håndtering af requestet. isPending afspejler den reelle status for formularindsendelsen – inklusive den native, før-hydrerings-POST, ikke kun klientsidens kald. state.error er direkte det, serverfunktionen returnerede – ingen JSON-parsing af svar, ingen try/catch omkring fetch. revalidatePath("/todos") i aktionen beder Next.js om at genopfriske cachen for den rute, så opgavelisten på siden viser den nye post uden manuel opdatering af klienttilstanden – det er frameworket, ikke dig, der beslutter, hvornår og hvordan den nye RSC-payload skal trækkes ind.
Faldgruber og god praksis
- Enhver funktion med
"use server"er et offentligt HTTP-endepunkt – ikke en privat funktion. Det er den mest almindelige sikkerhedsfejl ved Server Actions: fordi intet i UI'et direkte linker til den, virker den "skjult". Det er den ikke – kompileren genererer en varig, udefra kaldbar aktions-identifikator til den, så enhver, der finder den (fx ved at kigge på netværkstrafikken i DevTools), kan kalde den direkte, uden om dit UI og dets antagelser. Validering af inputdata og kontrol af rettigheder (session, brugerrolle) skal udføres inde i selve aktionen, præcis som ved et klassisk API-endepunkt – stol aldrig på, at "knappen er usynlig for ikke-loggede-ind-brugere". - Argumenter og returværdi skal kunne serialiseres. Server Actions sender data over netværket i Reacts serialiseringsformat (rigere end JSON – det understøtter blandt andet
Date,Map,FormDatasendt direkte fra formularen), men lukker stadig funktioner, klasseinstanser eller referencer til DOM-objekter ude. Bruger du.bind()til at lukke et ekstra argument inde ved kald fra klienten (addTodoAction.bind(null, listId)), skal du huske, atlistIdogså skal kunne serialiseres. - Det er stadig en reel netværksrequest, ikke et lokalt funktionskald. Det er let at glemme, fordi
await minAction(data)syntaktisk ligner et almindeligt JS-funktionskald til forveksling. Under motorhjelmen er det altid en fuld tur: serialisering, HTTP, deserialisering i den anden ende. At kalde en Server Action i en løkke for hvert tegn, der skrives i et tekstfelt (fx til løbende validering), forvandler din formular til en request-generator, ikke til et responsivt input – til den slags tilfælde er debounce lige så nødvendigt som ved ethvert andet netværkskald. - Manglende
revalidatePath/revalidateTagefterlader UI'et med en forældet cache. Next.js cacher som standard sidedata – en aktion, der gemmer noget i databasen, men ikke fortæller frameworket, hvilken rute der skal genopfriskes, resulterer ofte i klagen "det blev gemt, men vises ikke" – data er korrekte i databasen, men UI'et viser stadig den gamle, bufrede version af siden.
Opsummering
Server Actions er ikke "fetch med pænere syntaks" – det er to forskellige ting bygget på den samme mekanisme. For det første bygger de på browserens native indsendelse af formularer, så formularen virker, selv før JavaScript når at indlæses – du skal ikke skrive en eneste linje kode ansvarlig for den adfærd, du får den fra selve arkitekturen i <form action={...}>. For det andet, og vigtigere set fra et sikkerhedsperspektiv: enhver funktion mærket "use server" bliver et reelt, udefra kaldbart netværksendepunkt, uanset om noget synligt UI-element kalder den. At behandle den som en lokal, betroet funktion – uden validering og autorisation indeni – er den nemmeste måde at få en bekvem syntaks til at ende som et usikret API-endepunkt.