Meistern Sie die Validierung von React Server Actions. Ein tiefer Einblick in Formularverarbeitung, Sicherheits-Best-Practices und fortgeschrittene Techniken mit Zod, useFormState und useFormStatus.
Validierung von React Server Actions: Ein umfassender Leitfaden zur Verarbeitung und Sicherheit von Formulareingaben
Die Einführung von React Server Actions markiert einen bedeutenden Paradigmenwechsel in der Full-Stack-Entwicklung mit Frameworks wie Next.js. Indem sie Client-Komponenten ermöglichen, serverseitige Funktionen direkt aufzurufen, können wir nun kohäsivere, effizientere und interaktivere Anwendungen mit weniger Boilerplate-Code erstellen. Diese leistungsstarke neue Abstraktion rückt jedoch eine entscheidende Verantwortung in den Vordergrund: robuste Eingabevalidierung und Sicherheit.
Wenn die Grenze zwischen Client und Server so nahtlos wird, ist es leicht, die grundlegenden Prinzipien der Websicherheit zu übersehen. Jede Eingabe, die von einem Benutzer kommt, ist nicht vertrauenswürdig und muss auf dem Server rigoros überprüft werden. Dieser Leitfaden bietet eine umfassende Untersuchung der Verarbeitung und Validierung von Formulareingaben innerhalb von React Server Actions und deckt alles von grundlegenden Prinzipien bis hin zu fortgeschrittenen, produktionsreifen Mustern ab, die sicherstellen, dass Ihre Anwendung sowohl benutzerfreundlich als auch sicher ist.
Was genau sind React Server Actions?
Bevor wir uns der Validierung widmen, lassen Sie uns kurz zusammenfassen, was Server Actions sind. Im Wesentlichen sind es Funktionen, die Sie auf dem Server definieren, aber vom Client ausführen können. Wenn ein Benutzer ein Formular absendet oder auf eine Schaltfläche klickt, kann eine Server Action direkt aufgerufen werden, wodurch die Notwendigkeit entfällt, manuell API-Endpunkte zu erstellen, `fetch`-Anfragen zu behandeln und Lade-/Fehlerzustände zu verwalten.
Sie basieren auf dem Fundament von HTML-Formularen und der `FormData`-API der Web-Plattform, was sie standardmäßig progressiv erweiterbar macht. Das bedeutet, dass Ihre Formulare auch dann funktionieren, wenn JavaScript nicht geladen werden kann, was eine widerstandsfähige Benutzererfahrung bietet.
Beispiel einer einfachen Server Action:
// app/actions.js
'use server';
export async function createUser(formData) {
const name = formData.get('name');
const email = formData.get('email');
// ... Logik zum Speichern des Benutzers in der Datenbank
console.log('Creating user:', { name, email });
}
// app/page.js
import { createUser } from './actions';
export default function UserForm() {
return (
);
}
Diese Einfachheit ist mächtig, verbirgt aber auch die Komplexität dessen, was geschieht. Die `createUser`-Funktion wird ausschließlich auf dem Server ausgeführt, wird aber von einer Client-Komponente aufgerufen. Genau diese direkte Verbindung zu Ihrer Server-Logik ist der Grund, warum Validierung nicht nur ein Feature ist – sie ist eine Anforderung.
Die unerschütterliche Bedeutung der Validierung
In der Welt der Server Actions ist jede Funktion ein offenes Tor zu Ihrem Server. Eine ordnungsgemäße Validierung fungiert als Wächter an diesem Tor. Hier sind die Gründe, warum sie nicht verhandelbar ist:
- Datenintegrität: Ihre Datenbank und der Anwendungszustand sind auf saubere, vorhersagbare Daten angewiesen. Die Validierung stellt sicher, dass Sie keine fehlerhaften E-Mail-Adressen, leere Zeichenketten, wo Namen sein sollten, oder Text in einem Feld für Zahlen speichern.
- Verbesserte Benutzererfahrung (UX): Benutzer machen Fehler. Klare, sofortige und kontextspezifische Fehlermeldungen leiten sie an, ihre Eingaben zu korrigieren, was Frustration reduziert und die Abschlussraten von Formularen verbessert.
- Eisenharte Sicherheit: Dies ist der kritischste Aspekt. Ohne serverseitige Validierung ist Ihre Anwendung anfällig für eine Vielzahl von Angriffen, darunter:
- SQL-Injection: Ein böswilliger Akteur könnte SQL-Befehle in ein Formularfeld eingeben, um Ihre Datenbank zu manipulieren.
- Cross-Site Scripting (XSS): Wenn Sie nicht bereinigte Benutzereingaben speichern und rendern, könnte ein Angreifer bösartige Skripte einschleusen, die in den Browsern anderer Benutzer ausgeführt werden.
- Denial of Service (DoS): Das Senden unerwartet großer oder rechenintensiver Daten könnte Ihre Serverressourcen überlasten.
Clientseitige vs. serverseitige Validierung: Eine notwendige Partnerschaft
Es ist wichtig zu verstehen, dass die Validierung an zwei Stellen stattfinden sollte:
- Clientseitige Validierung: Diese dient der UX. Sie gibt sofortiges Feedback ohne einen Netzwerk-Roundtrip. Sie können einfache HTML5-Attribute wie `required`, `minLength`, `pattern` oder JavaScript verwenden, um Formate während der Eingabe des Benutzers zu überprüfen. Sie kann jedoch leicht umgangen werden, indem JavaScript deaktiviert oder Entwickler-Tools verwendet werden.
- Serverseitige Validierung: Diese dient der Sicherheit und Datenintegrität. Sie ist die ultimative Wahrheitsquelle Ihrer Anwendung. Egal was auf dem Client passiert, der Server muss alles, was er empfängt, erneut validieren. Server Actions sind der perfekte Ort, um diese Logik zu implementieren.
Faustregel: Verwenden Sie clientseitige Validierung für eine bessere Benutzererfahrung, aber vertrauen Sie für die Sicherheit immer nur der serverseitigen Validierung.
Implementierung der Validierung in Server Actions: Von einfach bis fortgeschritten
Lassen Sie uns unsere Validierungsstrategie aufbauen, beginnend mit einem einfachen Ansatz und übergehend zu einer robusteren, skalierbaren Lösung mit modernen Werkzeugen.
Ansatz 1: Manuelle Validierung und Rückgabe eines Zustands
Der einfachste Weg, die Validierung zu handhaben, besteht darin, `if`-Anweisungen in Ihrer Server Action hinzuzufügen und ein Objekt zurückzugeben, das Erfolg oder Misserfolg anzeigt.
// 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 zum Erstellen der Rechnung in der Datenbank
console.log('Invoice created for', customerName, 'with amount', amount);
redirect('/dashboard/invoices');
}
Dieser Ansatz funktioniert, hat aber einen großen UX-Nachteil: Er erfordert einen vollständigen Seiten-Reload, um die Fehlermeldung anzuzeigen. Wir können die Meldung nicht einfach auf der Formularseite selbst anzeigen. Hier kommen die React-Hooks für Server Actions ins Spiel.
Ansatz 2: Verwendung von `useFormState` für nahtloses Fehlerhandling
Der `useFormState`-Hook wurde speziell für diesen Zweck entwickelt. Er ermöglicht einer Server Action, einen Zustand zurückzugeben, der zur Aktualisierung der Benutzeroberfläche ohne ein vollständiges Navigationsereignis verwendet werden kann. Er ist der Eckpfeiler der modernen Formularbehandlung mit Server Actions.
Lassen Sie uns unser Formular zur Rechnungserstellung refaktorieren.
Schritt 1: Die Server Action aktualisieren
Die Action muss nun zwei Argumente akzeptieren: `prevState` und `formData`. Sie sollte ein neues Zustandsobjekt zurückgeben, das `useFormState` zur Aktualisierung der Komponente verwendet.
// app/actions.js
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
// Die anfängliche Zustandsform definieren
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 zum Speichern in der Datenbank
console.log('Invoice created successfully!');
} catch (e) {
return {
message: 'Database Error: Failed to create invoice.',
errors: {},
};
}
// Den Cache für die Rechnungsseite neu validieren und umleiten
revalidatePath('/dashboard/invoices');
redirect('/dashboard/invoices');
}
Schritt 2: Die Formular-Komponente mit `useFormState` aktualisieren
In unserer Client-Komponente verwenden wir den Hook, um den Zustand des Formulars zu verwalten und Fehler anzuzeigen.
// 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 (
);
}
Wenn der Benutzer nun ein ungültiges Formular absendet, wird die Server Action ausgeführt, gibt das Fehlerobjekt zurück und `useFormState` aktualisiert die `state`-Variable. Die Komponente wird neu gerendert und zeigt die spezifischen Fehlermeldungen direkt neben den entsprechenden Feldern an – alles ohne einen Seiten-Reload. Das ist eine enorme UX-Verbesserung!
Ansatz 3: UX-Verbesserung mit `useFormStatus`
Was passiert, während die Server Action ausgeführt wird? Der Benutzer könnte den Senden-Button mehrmals anklicken. Wir können Feedback mit dem `useFormStatus`-Hook geben, der uns Informationen über den Status der letzten Formularübermittlung liefert.
Wichtig: `useFormStatus` muss in einer Komponente verwendet werden, die ein Kind des `