Bliv ekspert i validering af React Server Actions. Dyk ned i formularbehandling, bedste sikkerhedspraksis og avancerede teknikker med Zod, useFormState og useFormStatus.
Validering af React Server Actions: En Komplet Guide til Formularbehandling og Sikkerhed
Introduktionen af React Server Actions har markeret et betydeligt paradigmeskifte inden for full-stack udvikling med frameworks som Next.js. Ved at lade klientkomponenter direkte kalde server-side funktioner, kan vi nu bygge mere sammenhængende, effektive og interaktive applikationer med mindre boilerplate-kode. Denne nye, kraftfulde abstraktion bringer dog et kritisk ansvar i forgrunden: robust inputvalidering og sikkerhed.
Når grænsen mellem klient og server bliver så sømløs, er det let at overse de grundlæggende principper for websikkerhed. Ethvert input, der kommer fra en bruger, er upålideligt og skal verificeres strengt på serveren. Denne guide giver en omfattende gennemgang af behandling og validering af formularinput i React Server Actions, og dækker alt fra grundlæggende principper til avancerede, produktionsklare mønstre, der sikrer, at din applikation er både brugervenlig og sikker.
Hvad er React Server Actions Præcist?
Før vi dykker ned i validering, lad os kort opsummere, hvad Server Actions er. I bund og grund er de funktioner, som du definerer på serveren, men kan udføre fra klienten. Når en bruger indsender en formular eller klikker på en knap, kan en Server Action kaldes direkte, hvilket fjerner behovet for manuelt at oprette API-endepunkter, håndtere `fetch`-anmodninger og administrere loading/fejltilstande.
De er bygget på fundamentet af HTML-formularer og webplatformens `FormData` API, hvilket gør dem progressivt forbedrede som standard. Dette betyder, at dine formularer vil fungere, selvom JavaScript ikke indlæses, hvilket giver en robust brugeroplevelse.
Eksempel på en grundlæggende Server Action:
// app/actions.js
'use server';
export async function createUser(formData) {
const name = formData.get('name');
const email = formData.get('email');
// ... logik til at gemme bruger i databasen
console.log('Creating user:', { name, email });
}
// app/page.js
import { createUser } from './actions';
export default function UserForm() {
return (
);
}
Denne enkelhed er kraftfuld, men den skjuler også kompleksiteten af, hvad der sker. `createUser`-funktionen kører udelukkende på serveren, men den kaldes fra en klientkomponent. Denne direkte linje til din serverlogik er præcis grunden til, at validering ikke bare er en funktion – det er et krav.
Den Ubøjelige Vigtighed af Validering
I verdenen af Server Actions er hver funktion en åben port til din server. Korrekt validering fungerer som vagten ved den port. Her er hvorfor det er ufravigeligt:
- Dataintegritet: Din database og applikationstilstand afhænger af rene, forudsigelige data. Validering sikrer, at du ikke gemmer misdannede e-mailadresser, tomme strenge hvor navne skulle være, eller tekst i et felt beregnet til tal.
- Forbedret Brugeroplevelse (UX): Brugere laver fejl. Klare, øjeblikkelige og kontekstspecifikke fejlmeddelelser guider dem til at rette deres input, hvilket reducerer frustration og forbedrer formularfuldførelsesrater.
- Robust Sikkerhed: Dette er det mest kritiske aspekt. Uden server-side validering er din applikation sårbar over for en række angreb, herunder:
- SQL Injection: En ondsindet aktør kunne indsende SQL-kommandoer i et formularfelt for at manipulere din database.
- Cross-Site Scripting (XSS): Hvis du gemmer og gengiver ikke-saniteret brugerinput, kan en angriber injicere ondsindede scripts, der udføres i andre brugeres browsere.
- Denial of Service (DoS): Indsendelse af uventet store eller beregningsmæssigt dyre data kunne overvælde dine serverressourcer.
Klientside- vs. Serverside-validering: Et Nødvendigt Partnerskab
Det er vigtigt at forstå, at validering bør ske to steder:
- Klientside-validering: Dette er for UX. Det giver øjeblikkelig feedback uden en netværksrundtur. Du kan bruge simple HTML5-attributter som `required`, `minLength`, `pattern` eller JavaScript til at kontrollere formater, mens brugeren skriver. Det kan dog let omgås ved at deaktivere JavaScript eller bruge udviklerværktøjer.
- Serverside-validering: Dette er for sikkerhed og dataintegritet. Det er din applikations ultimative sandhedskilde. Uanset hvad der sker på klienten, skal serveren genvalidere alt, hvad den modtager. Server Actions er det perfekte sted at implementere denne logik.
Tommelfingerregel: Brug klientside-validering for en bedre brugeroplevelse, men stol altid kun på serverside-validering for sikkerhed.
Implementering af Validering i Server Actions: Fra Grundlæggende til Avanceret
Lad os bygge vores valideringsstrategi op, startende med en simpel tilgang og gå videre til en mere robust, skalerbar løsning ved hjælp af moderne værktøjer.
Tilgang 1: Manuel Validering og Returnering af State
Den enkleste måde at håndtere validering på er at tilføje `if`-sætninger inde i din Server Action og returnere et objekt, der angiver succes eller fiasko.
// app/actions.js
'use server';
import { redirect } from 'next/navigation';
export async function createInvoice(formData) {
const customerName = formData.get('customerName');
const amount = formData.get('amount');
if (!customerName || customerName.trim() === '') {
return { success: false, message: 'Customer name is required.' };
}
if (!amount || isNaN(Number(amount)) || Number(amount) <= 0) {
return { success: false, message: 'Please enter a valid amount greater than zero.' };
}
// ... logik til at oprette fakturaen i databasen
console.log('Invoice created for', customerName, 'with amount', amount);
redirect('/dashboard/invoices');
}
Denne tilgang virker, men den har en stor UX-fejl: den kræver en fuld sidegenindlæsning for at vise fejlmeddelelsen. Vi kan ikke nemt vise meddelelsen på selve formularsiden. Det er her, Reacts hooks til Server Actions kommer ind i billedet.
Tilgang 2: Brug af `useFormState` til Problemfri Fejlhåndtering
`useFormState`-hook'et er designet specifikt til dette formål. Det tillader en Server Action at returnere en tilstand (state), der kan bruges til at opdatere UI'en uden en fuld navigationshændelse. Det er hjørnestenen i moderne formularhåndtering med Server Actions.
Lad os refaktorere vores formular til oprettelse af fakturaer.
Trin 1: Opdater Server Action
Action'en skal nu acceptere to argumenter: `prevState` og `formData`. Den skal returnere et nyt state-objekt, som `useFormState` vil bruge til at opdatere komponenten.
// app/actions.js
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
// Definer den oprindelige state-form
const initialState = {
message: null,
errors: {},
};
export async function createInvoice(prevState, formData) {
const customerName = formData.get('customerName');
const amount = formData.get('amount');
const status = formData.get('status');
const errors = {};
if (!customerName || customerName.trim().length < 2) {
errors.customerName = 'Customer name must be at least 2 characters.';
}
if (!amount || isNaN(Number(amount)) || Number(amount) <= 0) {
errors.amount = 'Please enter a valid amount.';
}
if (status !== 'pending' && status !== 'paid') {
errors.status = 'Please select a valid status.';
}
if (Object.keys(errors).length > 0) {
return {
message: 'Failed to create invoice. Please check the fields.',
errors,
};
}
try {
// ... logik til at gemme i databasen
console.log('Invoice created successfully!');
} catch (e) {
return {
message: 'Database Error: Failed to create invoice.',
errors: {},
};
}
// Genvalider cachen for fakturasiden og omdiriger
revalidatePath('/dashboard/invoices');
redirect('/dashboard/invoices');
}
Trin 2: Opdater Formular-komponenten med `useFormState`
I vores klientkomponent bruger vi hook'et til at administrere formularens tilstand og vise fejl.
// app/ui/invoices/create-form.js
'use client';
import { useFormState } from 'react-dom';
import { createInvoice } from '@/app/actions';
const initialState = {
message: null,
errors: {},
};
export function CreateInvoiceForm() {
const [state, dispatch] = useFormState(createInvoice, initialState);
return (
);
}
Nu, når brugeren indsender en ugyldig formular, kører Server Action, returnerer fejl-objektet, og `useFormState` opdaterer `state`-variablen. Komponenten gen-renderes og viser de specifikke fejlmeddelelser lige ved siden af de tilsvarende felter – alt sammen uden en sidegenindlæsning. Dette er en kæmpe UX-forbedring!
Tilgang 3: Forbedring af UX med `useFormStatus`
Hvad sker der, mens Server Action kører? Brugeren kan klikke på submit-knappen flere gange. Vi kan give feedback ved hjælp af `useFormStatus`-hook'et, som giver os information om status for den seneste formularindsendelse.
Vigtigt: `useFormStatus` skal bruges i en komponent, der er et barn af <form>-elementet.
// app/ui/submit-button.js
'use client';
import { useFormStatus } from 'react-dom';
export function SubmitButton() {
const { pending } = useFormStatus();
return (
);
}
// I din CreateInvoiceForm-komponent:
import { SubmitButton } from './submit-button';
// ...
Med denne simple tilføjelse vil submit-knappen nu være deaktiveret og vise "Creating...", mens serveren behandler anmodningen, hvilket forhindrer dobbelte indsendelser og giver klar feedback til brugeren.
Tilgang 4: Produktionsklar Validering med Zod
Manuelle `if`-tjek er fine til simple formularer, men for komplekse applikationer bliver de omstændelige, fejlbehæftede og svære at vedligeholde. Det er her, skemavalideringsbiblioteker som Zod, Yup eller Valibot skinner.
Zod er et TypeScript-first skema-deklarations- og valideringsbibliotek. Det giver dig mulighed for at definere et enkelt skema for dine data, som kan bruges på både server og klient, hvilket sikrer konsistens og typesikkerhed.
Trin 1: Definer et Zod-skema
Lad os definere et skema for vores fakturaformular. Dette bliver den eneste kilde til sandhed for, hvad der udgør en gyldig faktura.
// app/lib/schemas.js
import { z } from 'zod';
export const InvoiceSchema = z.object({
id: z.string(), // Vi bruger denne til opdateringer, men ikke oprettelse
customerId: z.string({ invalid_type_error: 'Please select a customer.' }),
amount: z.coerce
.number()
.gt(0, { message: 'Please enter an amount greater than $0.' }),
status: z.enum(['pending', 'paid'], {
invalid_type_error: 'Please select an invoice status.',
}),
date: z.string(),
});
export const CreateInvoice = InvoiceSchema.omit({ id: true, date: true });
Trin 2: Brug Skemaet i din Server Action
Nu kan vi erstatte alle vores manuelle tjek med et enkelt kald til vores Zod-skema. Zods `safeParse`-metode vil enten returnere de validerede data eller et detaljeret fejl-objekt.
// app/actions.js
'use server';
import { z } from 'zod';
import { CreateInvoice } from '@/app/lib/schemas';
export async function createInvoiceWithZod(prevState, formData) {
// 1. Valider formularfelter med Zod
const validatedFields = CreateInvoice.safeParse({
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
});
// 2. Hvis validering fejler, returner fejl tidligt.
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: 'Missing Fields. Failed to Create Invoice.',
};
}
// 3. Forbered data til indsættelse i databasen
const { customerId, amount, status } = validatedFields.data;
const amountInCents = amount * 100;
const date = new Date().toISOString().split('T')[0];
// 4. Indsæt data i databasen
try {
// await sql`...` din databaseforespørgsel her
console.log('Successfully created invoice with validated data.');
} catch (error) {
return {
message: 'Database Error: Failed to Create Invoice.',
};
}
// 5. Genvalider og omdiriger
revalidatePath('/dashboard/invoices');
redirect('/dashboard/invoices');
}
Bemærk, hvor meget renere dette er. Valideringslogikken er deklarativ og samlet i vores skemafil. Action'ens ansvar er nu at orkestrere validering, databehandling og databaseinteraktion. `flatten().fieldErrors`-metoden fra Zod giver et perfekt struktureret objekt, der mapper feltnavne til et array af fejlmeddelelser, hvilket er præcis, hvad vores formularkomponent har brug for.
Kritiske Sikkerhedsbedste Praksis for Server Actions
At bruge et valideringsbibliotek er et massivt skridt fremad, men ægte sikkerhed kræver en tilgang i flere lag. Hver Server Action er en potentiel angrebsvektor.
1. Gå Altid Ud fra Ondsindet Hensigt
Stol aldrig, aldrig på brugerinput. Dette er den gyldne regel. Selv hvis din klientside-kode sender perfekte data, kan en angriber bruge værktøjer som `curl` eller Postman til at sende en håndlavet anmodning direkte til dit Server Action-endepunkt. Din server-side logik er dit eneste pålidelige forsvar.
- Valider Alt: Valider ikke kun formatet, men også forretningslogikken. Har denne bruger tilladelse til at oprette en faktura for *denne* kunde? Er beløbet inden for et rimeligt interval?
- Saniter til Output: Selvom React automatisk escaper data for at forhindre XSS ved rendering, skal du være opmærksom på at sanitere det, hvis du bruger data i andre sammenhænge (f.eks. generering af e-mails, logning) for at forhindre script-injektion i disse systemer.
2. Autentificering og Autorisering er Obligatorisk
Hver Server Action, der udfører en mutation (opret, opdater, slet) eller tilgår beskyttede data, skal verificere brugerens identitet og tilladelser.
Start altid din action med et sessionstjek:
// app/actions.js
'use server';
import { auth } from '@/auth'; // Antager, at du bruger NextAuth.js eller lignende
import { sql } from '@vercel/postgres';
export async function deleteInvoice(id) {
const session = await auth();
if (!session?.user) {
// Eller kast en fejl
return { message: 'Authentication required.' };
}
// Autorisationstjek: Har denne bruger tilladelse til at slette?
// Dette kan involvere kontrol af roller eller ejerskab.
const userRole = session.user.role;
if (userRole !== 'admin') {
return { message: 'Unauthorized action.' };
}
try {
await sql`DELETE FROM invoices WHERE id = ${id}`;
revalidatePath('/dashboard/invoices');
return { message: 'Deleted Invoice.' };
} catch (error) {
return { message: 'Database Error: Failed to Delete Invoice.' };
}
}
3. Beskyttelse mod CSRF (Cross-Site Request Forgery)
CSRF-angreb lurer en logget ind bruger til ubevidst at indsende en ondsindet anmodning til din applikation. Heldigvis har frameworks som Next.js indbygget CSRF-beskyttelse til Server Actions ved hjælp af en kombination af teknikker, herunder double-submit cookies og oprindelseskontroller. Selvom dette håndteres for dig, er det afgørende at være opmærksom på det og sikre, at du ikke utilsigtet skaber sårbarheder ved at fejlkonfigurere dit setup.
4. Implementer Rate Limiting
En ondsindet bruger eller bot kunne spamme dine formularindsendelser i forsøg på at brute-force et login, udtømme dine databaseforbindelser eller fylde dit system med junk-data. Implementering af rate limiting på kritiske Server Actions er afgørende.
Du kan bruge tjenester som Upstash Rate Limiting eller implementere din egen logik ved hjælp af en key-value store som Redis. Nøglen er typisk brugerens IP-adresse eller bruger-ID.
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
// Opret en ny ratelimiter, der tillader 5 anmodninger pr. 10 sekunder
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(5, '10 s'),
});
export async function sensitiveAction(formData) {
const ip = headers().get('x-forwarded-for'); // Eller hent bruger-ID fra session
const { success } = await ratelimit.limit(ip);
if (!success) {
return { message: 'Too many requests. Please try again later.' };
}
// ... resten af action-logikken
}
Globale og Tilgængelighedsmæssige Overvejelser
At bygge for et globalt publikum kræver, at man tænker ud over den tekniske implementering.
Internationalisering (i18n) af Fejlmeddelelser
At hardcode fejlmeddelelser på engelsk er ikke ideelt for en global applikation. En bedre tilgang er at lade din Server Action returnere fejl-*koder* eller *nøgler*.
// I action'en
if (!validatedFields.success) {
// ...
return { errors: { customerId: ['error.customer.required'] } };
}
// I klientkomponenten, med et bibliotek som 'react-i18next'
import { useTranslation } from 'react-i18next';
function MyForm() {
const { t } = useTranslation();
const [state, dispatch] = useFormState(...);
// ...
{state.errors?.customerId &&
{t(state.errors.customerId[0])}
}
}
Dette adskiller din valideringslogik fra din præsentation og lader din front-end håndtere oversættelser problemfrit.
Tilgængelighed (a11y)
Når du viser fejl, skal du sikre dig, at de er tilgængelige for brugere af hjælpeteknologier. Forbind fejlmeddelelser med deres tilsvarende formularinput ved hjælp af `aria-describedby`-attributten.
{state.errors?.customerName &&
state.errors.customerName.map((error) => (
{error}
))}
`aria-live="polite"`-attributten vil sikre, at skærmlæsere annoncerer fejlmeddelelsen, når den vises.
Konklusion: En Ny Æra af Ansvar
React Server Actions er et kraftfuldt værktøj, der strømliner full-stack udvikling og bringer serverlogik tættere på UI'en end nogensinde før. Denne magt kræver dog en disciplineret og sikkerhedsfokuseret tankegang.
Ved at udnytte hooks som `useFormState` og `useFormStatus` kan vi skabe progressivt forbedrede formularer med en fremragende brugeroplevelse. Ved at integrere robuste skemavalideringsbiblioteker som Zod kan vi skrive ren, vedligeholdelsesvenlig og typesikker valideringslogik. Og ved at overholde grundlæggende sikkerhedsprincipper – autentificering, autorisation, rate limiting og altid validere på serveren – kan vi bygge applikationer, der ikke kun er elegante og effektive, men også robuste og sikre.
Behandl hver Server Action som et offentligt API-endepunkt. Valider dens input, kontroller dens tilladelser, og håndter dens fejl elegant. Ved at gøre det kan du trygt udnytte det fulde potentiale i dette spændende nye kapitel i React-udvikling.