Server Actions i Next.js: skjemaet som fungerer før JavaScript rekker å laste
Server Actions i Next.js: skjemaet som fungerer før JavaScript rekker å laste
Det klassiske skjemamønsteret i React: onSubmit, event.preventDefault(), fetch('/api/todos', { method: 'POST', body: JSON.stringify(...) }), useState for å håndtere lastetilstand. Det fungerer – under én forutsetning som er lett å glemme, fordi den aldri viser seg på din egen laptop med rask tilkobling: JavaScript må rekke å laste, parses og kjøre, før skjemaet i det hele tatt begynner å reagere på et klikk. På en treg mobiltilkobling, midt i hydreringen av en stor side, eller når skriptet blir blokkert av en nettleserutvidelse – brukeren klikker «Send», og bokstavelig talt ingenting skjer. Ingen feilmelding, ingen spinner – onClick finnes rett og slett ikke ennå.
Server Actions i Next.js blir noen ganger presentert som en mer bekvem syntaks for nøyaktig det samme kallet – «fetch, bare at du slipper å skrive en API-rute». Det er en forenkling som overser det som faktisk er nytt i mekanismen: <form action={minAction}> bygger på nettleserens innebygde, alltid eksisterende mekanisme for å sende inn skjemaer, ikke på JavaScript som en nødvendig forutsetning. Det endrer både hvordan det fungerer under panseret, og hvilke feil team i praksis gjør når de behandler "use server" som et vanlig kall til en lokal funksjon.
Hva direktivet "use server" faktisk gjør
Når du merker en funksjon med "use server", lar Next.js-kompilatoren ikke kroppen bli liggende i pakken som nettleseren mottar. Den kutter den ut og erstatter den med en referanse – en kryptert identifikator som peker på hvilken funksjon som skal kalles på serveren. Det som faktisk havner hos klienten, er en liten stub: «når du kaller meg, send en POST til et spesielt RSC-endepunkt med denne identifikatoren og de serialiserte argumentene». Selve logikken – spørringen mot databasen, valideringen, uansett hva funksjonen gjør – forlater aldri 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: "Tittelen kan ikke være tom." };}await db.todo.create({ data: { title: title.trim() } });revalidatePath("/todos");return { error: null };}
Når du fester denne funksjonen til <form action={addTodoAction}>, skjer det noe som et vanlig fetch aldri har gitt deg gratis: det fungerer før noe som helst JavaScript har kjørt. Nettleseren har alltid kunnet sende et skjema som en nativ POST hvis attributtet action peker på en URL – React 19 og Next.js utvider dette slik at action også kan være en serverfunksjon, og nettleseren følger uansett sin native rute hvis JS ikke er klar ennå. Når JavaScript først kjører, fanger Next.js opp hendelsen og gjør det samme via fetch i bakgrunnen, uten å laste siden på nytt, med en jevn oppdatering av brukergrensesnittet. Dette er progressiv forbedring innebygd i rammeverket, ikke noe du selv må programmere – nøyaktig samme mekanisme som vi skrev om i forbindelse med RouteAnnouncer i artikkelen om tilgjengelighet, bare brukt på datamutasjon i stedet for navigasjon.
Praktisk eksempel: et skjema med full tilstandshåndtering, uten manuell fetch
React 19 la til useActionState spesielt for dette mønsteret – den kobler kallet til en server-action sammen med tilstanden returnert fra siste kjøring, uten en manuell useState for å lagre feil eller lastestatus.
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 oppgave" required /><button type="submit" disabled={isPending}>{isPending ? "Legger til…" : "Legg til"}</button>{state.error && <p className="form-error">{state.error}</p>}</form>);};export default TodoForm;
Flere ting skjer her samtidig, uten en eneste linje kode viet til manuell håndtering av requesten. isPending gjenspeiler den faktiske tilstanden til skjemainnsendingen – inkludert den native, pre-hydrerings-POST-en, ikke bare kall fra klientsiden. state.error er direkte det som serverfunksjonen returnerte – ingen parsing av JSON-svar, ingen try/catch rundt fetch. revalidatePath("/todos") i action-en ber Next.js oppdatere cachen for denne ruten, så oppgavelisten på siden viser det nye elementet uten manuell oppdatering av klienttilstanden – det er rammeverket, ikke deg, som bestemmer når og hvordan den nye RSC-payloaden skal hentes inn.
Fallgruver og gode praksiser
- Hver funksjon merket
"use server"er et offentlig HTTP-endepunkt – ikke en privat funksjon. Dette er den vanligste sikkerhetsforvekslingen med Server Actions: siden ingenting i UI-et lenker direkte til den, virker den «skjult». Det er den ikke – kompilatoren genererer en varig, eksternt kallbar action-identifikator for den, så hvem som helst som får tak i den (for eksempel ved å se på nettverkstrafikken i DevTools) kan kalle den direkte, forbi UI-et ditt og dets forutsetninger. Validering av inndata og sjekk av rettigheter (sesjon, brukerrolle) må gjøres inne i selve action-en, akkurat som i et klassisk API-endepunkt – stol aldri på at «knappen er usynlig for ikke-innloggede». - Argumenter og returverdi må være serialiserbare. Server Actions sender data over nettverket i Reacts eget serialiseringsformat (rikere enn JSON – det håndterer blant annet
Date,Map,FormDatasendt direkte fra skjemaet), men slipper fortsatt ikke gjennom funksjoner, klasseinstanser eller referanser til DOM-objekter. Hvis du bruker.bind()for å knytte til et ekstra argument ved kall fra klienten (addTodoAction.bind(null, listId)), husk atlistIdogså må kunne serialiseres. - Det er fortsatt en reell nettverksrequest, ikke et lokalt funksjonskall. Det er lett å glemme, fordi
await minAction(data)syntaktisk ser identisk ut med et vanlig JS-funksjonskall. Under panseret er det alltid en full rundtur: serialisering, HTTP, deserialisering på den andre siden. Å kalle en Server Action i en løkke for hvert tegn som skrives i et tekstfelt (for eksempel for løpende validering) forvandler skjemaet ditt til en requestgenerator, ikke et responsivt input-felt – for slike tilfeller er debounce like nødvendig som ved ethvert annet nettverkskall. - Manglende
revalidatePath/revalidateTagetterlater UI-et med utdatert cache. Next.js cacher som standard sidedata – en action som lagrer noe i databasen, men ikke sier fra til rammeverket hvilken rute som skal oppdateres, resulterer ofte i tilbakemeldingen «det ble lagret, men vises ikke» – dataen er riktig i databasen, men UI-et viser fortsatt den gamle, bufrede versjonen av siden.
Oppsummering
Server Actions er ikke «fetch med penere syntaks» – det er to forskjellige ting bygget på samme mekanisme. For det første bygger de på nettleserens native innsending av skjemaer, så skjemaet fungerer selv før JavaScript rekker å laste – du trenger ikke skrive en eneste linje kode for denne oppførselen, du får den fra selve arkitekturen til <form action={...}>. For det andre, og viktigere fra et sikkerhetsperspektiv: hver funksjon merket "use server" blir et reelt, eksternt kallbart endepunkt på nettet, uavhengig av om noe synlig UI-element faktisk kaller den. Å behandle den som en lokal, betrodd funksjon – uten validering og autorisasjon inni – er den enkleste måten å forvandle en bekvem syntaks til et usikret API-endepunkt.