Next.js Server Actions Tutorial: Formularverarbeitung und Validierung – Best Practices

Am Rechner sitzen, Code für ein Registrierungsformular ansehen. Im Ordner liegen schon vier Dateien: Formular-Komponente, API Route, Typdefinitionen, Fehlerbehandlung … Für ein simples Formular-Submit sind fast 200 Zeilen zusammengekommen.
Gibt es keinen einfacheren Weg?
Die Antwort: Server Actions. Dieses Feature im Next.js App Router kann die Formularverarbeitung um bis zu 80 % vereinfachen. Keine API Route, kein manuelles fetch, kein umständliches State-Management. Klingt gut – aber Sie fragen sich vielleicht: Ist das wirklich sicher? Wie validiere ich? Wie handle ich Loading?
Am Anfang hatte ich dieselben Bedenken. Nach einigen Monaten Nutzung – mit ein paar Stolpersteinen – möchte ich die Praxis-Tipps für Next.js Server Actions bei Formularen teilen: vom einfachen Submit über Zod-Validierung und Sicherheit bis zur UX-Optimierung. Mit echten Codebeispielen, damit Sie schnell einsteigen.
Server Actions – Grundlagen
Was sind Server Actions?
Server Actions sind asynchrone Funktionen, die auf dem Server laufen. Sie markieren sie mit 'use server' und können sie direkt in der action-Eigenschaft eines Formulars verwenden. Beim Submit wird die Funktion automatisch aufgerufen – Datenverarbeitung, Datenbankoperationen, Cache-Updates: alles passiert serverseitig.
Die wichtigsten Eigenschaften:
- Typsicherheit: TypeScript prüft die gesamte Kette
- Zero Configuration: kein
/api-Ordner nötig - Automatische Verarbeitung: FormData wird automatisch übergeben
Zwei Schreibweisen: Server Action direkt in der Komponente (inline) oder in einer separaten Datei (Modul-Ebene):
// Variante 1: inline in der Komponente
export default function Page() {
async function createUser(formData: FormData) {
'use server' // als Server Action markieren
const name = formData.get('name')
// Daten verarbeiten...
}
return <form action={createUser}>...</form>
}
// Variante 2: separate Datei (empfohlen)
// app/actions.ts
'use server' // Markierung auf Datei-Ebene
export async function createUser(formData: FormData) {
const name = formData.get('name')
// Daten verarbeiten...
}
Die Frage: Was unterscheidet Server Actions von klassischen API Routes – und wann nutzt man welche?
Hier eine Vergleichstabelle:
| Eigenschaft | Server Actions | API Routes |
|---|---|---|
| Einsatz | Formular-Submit, Datenänderungen | RESTful API, externe Aufrufe |
| HTTP-Methode | nur POST | GET/POST/PUT/DELETE usw. |
| Typsicherheit | nativ typsicher | Typen manuell definieren |
| Aufruf | Funktion direkt aufrufen | fetch-Request |
| Typische Szenarien | interne Logik, Formulare | öffentliche API, Drittanbieter |
| Codeumfang | gering | relativ höher |
Kurz gesagt: intern Server Actions, extern API Routes. Für Formulare in der eigenen App reichen Server Actions. Für Schnittstellen an andere Systeme oder GET-Anfragen brauchen Sie API Routes.
Laut Vercel-Umfrage 2025 setzen bereits 63 % der Entwickler Server Actions in Produktion ein. Das ist kein Experiment mehr.
"63 % der Entwickler nutzen Server Actions bereits in Produktion"
Erstes Server-Actions-Beispiel
Direkt Code – ein einfaches Login-Formular:
// app/login/page.tsx
export default function LoginPage() {
async function handleLogin(formData: FormData) {
'use server' // als Server-Funktion markieren
// Daten aus dem Formular
const email = formData.get('email') as string
const password = formData.get('password') as string
// Login-Logik (vereinfacht)
console.log('Login-Versuch:', email)
// In der Praxis: Nutzer prüfen, Token erzeugen usw.
}
return (
<form action={handleLogin}>
<input
type="email"
name="email"
placeholder="E-Mail"
required
/>
<input
type="password"
name="password"
placeholder="Passwort"
required
/>
<button type="submit">Anmelden</button>
</form>
)
}
So einfach. Die Kernpunkte:
'use server': Next.js führt die Funktion serverseitig ausformData.get(): Wert über dasname-Attribut des Feldesaction={handleLogin}: beim Submit automatischer Aufruf
Beim Klick auf Absenden lädt die Seite nicht neu – die Daten gehen direkt an den Server. Weniger fetch, useState, Fehlerbehandlung als bei klassischen Ansätzen.
Das ist aber nur die Basis. In echten Projekten brauchen Sie Validierung, Fehleranzeige und Loading. Weiter geht’s.
Formularvalidierung in der Praxis
Validierung mit Zod
Nur required im Client? Zu optimistisch. Mit den Browser-DevTools lassen sich solche Prüfungen leicht umgehen. Serverseitige Validierung ist Pflicht.
Hier kommt Zod ins Spiel: Es prüft Datenformate serverseitig und liefert bei Fehlern sofort strukturierte Meldungen – bevor schlechte Daten in der Datenbank landen.
Zod installieren:
npm install zod
Validierungsregeln definieren:
// app/actions.ts
'use server'
import { z } from 'zod'
// Validierungs-Schema
const SignupSchema = z.object({
name: z.string().min(2, 'Name mindestens 2 Zeichen'),
email: z.string().email('Ungültiges E-Mail-Format'),
password: z.string().min(8, 'Passwort mindestens 8 Zeichen'),
})
export async function signup(formData: FormData) {
// Daten aus FormData extrahieren
const rawData = {
name: formData.get('name'),
email: formData.get('email'),
password: formData.get('password'),
}
// Validieren
const result = SignupSchema.safeParse(rawData)
// Bei Fehler zurückgeben
if (!result.success) {
return {
success: false,
errors: result.error.flatten().fieldErrors, // Fehler pro Feld
}
}
// Bei Erfolg: Geschäftslogik
const { name, email, password } = result.data
// Nutzer anlegen, in DB speichern usw.
console.log('Nutzer anlegen:', { name, email })
return {
success: true,
message: 'Registrierung erfolgreich!',
}
}
Wichtige Punkte:
safeParsewirft keine Exception: bei Fehler{ success: false, error: ... }– saubere Fehlerbehandlungflatten().fieldErrors: Fehler als{ name: ['Fehler1'], email: ['Fehler2'] }– gut für die Anzeige- Strukturierte Antwort: mit
success-Flag und Fehlern – der Client entscheidet, was angezeigt wird
Noch offen: Wie zeigen Sie die Fehler im Formular? Dafür brauchen Sie useActionState.
Validierungsfehler anzeigen: useActionState
useActionState ist ein React-19-Hook (früher useFormState) für den State, den Server Actions zurückgeben. Er:
- speichert die Server-Antwort im Komponenten-State
- liefert eine gewrappte action-Funktion
- zeigt an, ob gerade gesendet wird
Code:
// app/signup/page.tsx
'use client' // Hook erfordert Client-Komponente
import { useActionState } from 'react'
import { signup } from '@/app/actions'
export default function SignupPage() {
// Initialzustand
const initialState = { success: false, errors: {}, message: '' }
// useActionState: Server Action + Initialzustand
const [state, formAction, isPending] = useActionState(signup, initialState)
return (
<form action={formAction}> {/* formAction statt roher action */}
<div>
<label>Name</label>
<input
type="text"
name="name"
required
/>
{/* Feldfehler anzeigen */}
{state.errors?.name && (
<p className="error">{state.errors.name[0]}</p>
)}
</div>
<div>
<label>E-Mail</label>
<input
type="email"
name="email"
required
/>
{state.errors?.email && (
<p className="error">{state.errors.email[0]}</p>
)}
</div>
<div>
<label>Passwort</label>
<input
type="password"
name="password"
required
/>
{state.errors?.password && (
<p className="error">{state.errors.password[0]}</p>
)}
</div>
<button type="submit" disabled={isPending}>
{isPending ? 'Wird gesendet...' : 'Registrieren'}
</button>
{/* Erfolgsmeldung */}
{state.success && (
<p className="success">{state.message}</p>
)}
</form>
)
}
Ablauf:
- Nutzer sendet ab →
signupwird aufgerufen - Validierung schlägt fehl →
{ success: false, errors: {...} } useActionStatespeichert das instate- Komponente rendert neu, Fehler werden angezeigt
isPending ist beim Senden true, danach wieder false – für deaktivierten Button und Loading-Text.
Hinweis: Nach Validierungsfehlern gehen eingegebene Werte oft verloren. Sie können beim Zurückgeben ein values-Feld mitgeben und defaultValue setzen – hier der Fokus auf useActionState: Client-Komponente und Server Actions verbinden, State-Management vereinfachen.
Benutzererfahrung optimieren
Loading-Status und Doppel-Submit verhindern
Oben haben wir isPending für Loading genutzt – es gibt noch useFormStatus. Die beiden verwechselt man leicht.
Kurz:
isPendingvonuseActionState: in der Formular-KomponentependingvonuseFormStatus: in Kind-Komponenten (z. B. Submit-Button)
useFormStatus darf nur in <form>-Kindern aufgerufen werden, nicht in der Formular-Komponente selbst. Das klingt umständlich, erlaubt aber wiederverwendbare Button-Komponenten.
Beispiel – Submit-Button auslagern:
// components/SubmitButton.tsx
'use client'
import { useFormStatus } from 'react-dom'
export function SubmitButton({ children }: { children: React.ReactNode }) {
const { pending } = useFormStatus() // Submit-Status
return (
<button
type="submit"
disabled={pending}
className={pending ? 'loading' : ''}
>
{pending ? 'Wird gesendet...' : children}
</button>
)
}
Im Formular:
// app/signup/page.tsx
'use client'
import { useActionState } from 'react'
import { signup } from '@/app/actions'
import { SubmitButton } from '@/components/SubmitButton'
export default function SignupPage() {
const [state, formAction] = useActionState(signup, { success: false, errors: {} })
return (
<form action={formAction}>
{/* Formularfelder... */}
<SubmitButton>Registrieren</SubmitButton> {/* Loading automatisch */}
{state.errors?.general && (
<p className="error">{state.errors.general}</p>
)}
</form>
)
}
Die Loading-Logik steckt im Button. Beim Senden:
- Button wird deaktiviert (kein Doppel-Submit)
- Text wechselt zu „Wird gesendet…“
- optional Spinner-Animation
Unterschied pending vs. isPending?
| Eigenschaft | isPending (useActionState) | pending (useFormStatus) |
|---|---|---|
| Aufrufort | Formular-Komponente | Kind der Formular-Komponente |
| Einsatz | Gesamt-State des Formulars | unabhängiger Submit-Button |
| Flexibilität | state und pending zusammen | nur pending |
In Projekten nutze ich:
- komplexe Formularlogik, mehrere States →
useActionState - generischer Submit-Button →
useFormStatus
Progressive Enhancement
Server Actions unterstützen Progressive Enhancement: Auch ohne JavaScript funktioniert das Formular.
Technisch nutzen Server Actions das native <form>-Submit. Mit JavaScript fängt Next.js ab und macht daraus eine AJAX-Anfrage; ohne JavaScript bleibt klassisches Formular-Submit.
In der Praxis selten relevant – aber plus für Barrierefreiheit und Crawler. Next.js erledigt das automatisch.
Sicherheit und Best Practices
Sicherheit von Server Actions
Der am leichtesten übersehene Punkt. Viele denken: läuft auf dem Server, also sicher. Falsch.
Server Actions sind im Grunde öffentliche API-Endpunkte. Next.js vergibt eine schwer erratbare ID – das ist Obfuscation, keine echte Sicherheit. Mit DevTools und Netzwerk-Tab findet man die Action-ID und kann sie manuell aufrufen.
Next.js bietet eingebauten Schutz:
- CSRF-Schutz: Server Actions nur per POST; Origin- und Host-Header müssen passen – Cross-Site-Requests werden abgelehnt
- Sichere Action-ID: verschlüsselte ID pro Action, schwer zu enumerieren
- Verschlüsselung von Closure-Variablen: externe Variablen in Actions werden verschlüsselt
Das reicht nicht. Sie müssen zusätzlich:
1. Eingabevalidierung
Client-Daten nie blind vertrauen. Zod-Validierung ist Pflicht.
2. Authentifizierung
Prüfen, ob der Nutzer angemeldet ist. Jede geschützte Action braucht Session-Prüfung.
3. Autorisierung
Angemeldet heißt nicht berechtigt. Nutzer A darf nicht die Daten von Nutzer B löschen – Berechtigung pro Aktion prüfen.
Praxisbeispiel:
// app/actions.ts
'use server'
import { cookies } from 'next/headers'
import { z } from 'zod'
const DeletePostSchema = z.object({
postId: z.string().min(1),
})
export async function deletePost(formData: FormData) {
// 1. Eingabe validieren
const rawData = {
postId: formData.get('postId'),
}
const result = DeletePostSchema.safeParse(rawData)
if (!result.success) {
return { success: false, error: 'Ungültige Anfrage' }
}
const { postId } = result.data
// 2. Authentifizierung: angemeldet?
const cookieStore = await cookies()
const sessionToken = cookieStore.get('session')?.value
if (!sessionToken) {
return { success: false, error: 'Bitte zuerst anmelden' }
}
// 3. Aktuellen Nutzer laden
const currentUser = await getUserFromSession(sessionToken)
if (!currentUser) {
return { success: false, error: 'Sitzung abgelaufen' }
}
// 4. Autorisierung: gehört der Beitrag dem Nutzer?
const post = await getPost(postId)
if (!post) {
return { success: false, error: 'Beitrag existiert nicht' }
}
if (post.authorId !== currentUser.id) {
return { success: false, error: 'Keine Berechtigung zum Löschen' }
}
// 5. Aktion ausführen
await deletePostFromDB(postId)
return { success: true, message: 'Erfolgreich gelöscht' }
}
Vollständiger Ablauf: Validierung → Authentifizierung → Autorisierung → Aktion. Alles nötig.
Praktische Bibliothek: next-safe-action – Middleware für einheitliche Validierung, Auth und Fehlerbehandlung:
import { createSafeActionClient } from 'next-safe-action'
// Action-Client mit Auth
const actionClient = createSafeActionClient({
// Middleware: Login prüfen
async middleware() {
const session = await getSession()
if (!session) {
throw new Error('Nicht angemeldet')
}
return { userId: session.userId }
},
})
// Automatische Auth-Prüfung
export const deletePost = actionClient
.schema(DeletePostSchema)
.action(async ({ parsedInput, ctx }) => {
const { postId } = parsedInput
const { userId } = ctx // userId aus Middleware
// Löschen...
})
Alle geschützten Actions teilen sich dieselbe Logik – deutlich aufgeräumter Code.
Merken: Server Actions sind kein Zauber – es ist ein API-Endpunkt. Sicherheitsmaßnahmen bleiben Ihre Aufgabe.
Praxis: Formular mit Authentifizierung
Vollständiges Beispiel – Kommentarformular nur für angemeldete Nutzer:
// app/actions.ts
'use server'
import { cookies } from 'next/headers'
import { z } from 'zod'
import { revalidatePath } from 'next/cache'
const CommentSchema = z.object({
postId: z.string(),
content: z.string().min(1, 'Kommentar darf nicht leer sein').max(500, 'Maximal 500 Zeichen'),
})
export async function addComment(formData: FormData) {
// 1. Eingabe validieren
const rawData = {
postId: formData.get('postId'),
content: formData.get('content'),
}
const result = CommentSchema.safeParse(rawData)
if (!result.success) {
return {
success: false,
errors: result.error.flatten().fieldErrors,
}
}
// 2. Authentifizierung
const cookieStore = await cookies()
const sessionToken = cookieStore.get('session')?.value
if (!sessionToken) {
return {
success: false,
error: 'Bitte anmelden, um zu kommentieren',
}
}
const user = await getUserFromSession(sessionToken)
if (!user) {
return {
success: false,
error: 'Sitzung abgelaufen, bitte erneut anmelden',
}
}
// 3. Kommentar speichern
const { postId, content } = result.data
await saveComment({
postId,
content,
authorId: user.id,
authorName: user.name,
createdAt: new Date(),
})
// 4. Cache invalidieren – Kommentar sofort sichtbar
revalidatePath(`/posts/${postId}`)
return {
success: true,
message: 'Kommentar veröffentlicht',
}
}
Client-Komponente:
// app/posts/[id]/CommentForm.tsx
'use client'
import { useActionState } from 'react'
import { addComment } from '@/app/actions'
import { SubmitButton } from '@/components/SubmitButton'
export function CommentForm({ postId }: { postId: string }) {
const [state, formAction] = useActionState(addComment, {
success: false,
errors: {},
})
return (
<form action={formAction}>
{/* postId als verstecktes Feld */}
<input type="hidden" name="postId" value={postId} />
<textarea
name="content"
placeholder="Ihr Kommentar..."
rows={4}
required
/>
{state.errors?.content && (
<p className="error">{state.errors.content[0]}</p>
)}
{state.error && (
<p className="error">{state.error}</p>
)}
{state.success && (
<p className="success">{state.message}</p>
)}
<SubmitButton>Kommentar senden</SubmitButton>
</form>
)
}
Alles zusammen:
- Zod-Validierung
- Login-Prüfung
useActionStatefür StaterevalidatePathfür Cache- Submit-Button mit Loading
Produktionsreifer Formular-Workflow.
Fortgeschrittene Techniken
Zusätzliche Parameter übergeben
Manchmal brauchen Sie mehr als Formularfelder – z. B. Beitrags-ID beim Bearbeiten.
Eine Möglichkeit: verstecktes Feld
<input type="hidden" name="postId" value={postId} />
Eleganter: JavaScript-bind
// app/actions.ts
'use server'
export async function updatePost(postId: string, formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
// Beitrag aktualisieren...
await updatePostInDB(postId, { title, content })
return { success: true }
}
Client:
// app/posts/[id]/edit/page.tsx
'use client'
import { updatePost } from '@/app/actions'
export default function EditPost({ postId }: { postId: string }) {
// postId an erste Parameterposition binden
const updatePostWithId = updatePost.bind(null, postId)
return (
<form action={updatePostWithId}>
<input type="text" name="title" required />
<textarea name="content" required />
<button type="submit">Aktualisieren</button>
</form>
)
}
bind(null, postId) erzeugt eine neue Funktion mit festem erstem Argument. Beim Submit wird FormData als zweites Argument übergeben.
Typisch für Bearbeiten, Löschen und andere ID-basierte Aktionen.
Cache-Revalidierung
Nach Server Actions kann der Seiten-Cache veraltet sein. Next.js bietet zwei Funktionen:
1. revalidatePath
Nach Pfad invalidieren:
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) {
// Beitrag anlegen...
// Startseiten-Liste aktualisieren
revalidatePath('/')
// Detailseite aktualisieren
revalidatePath(`/posts/${newPostId}`)
return { success: true }
}
2. revalidateTag
Nach Tag invalidieren (Tags beim fetch setzen):
// Beim Laden taggen
fetch('https://api.example.com/posts', {
next: { tags: ['posts'] }
})
// In der Server Action alle 'posts'-Caches invalidieren
import { revalidateTag } from 'next/cache'
export async function createPost(formData: FormData) {
// Beitrag anlegen...
revalidateTag('posts') // alle zugehörigen Caches
return { success: true }
}
Wann welches?
- Feste, wenige Pfade →
revalidatePath - Daten über viele Seiten →
revalidateTag
Meist starte ich mit revalidatePath – einfach und direkt. Tags nur, wenn eine Aktion viele Seiten betrifft.
Optimistische Updates
Bei fast immer erfolgreichen Aktionen (Like, Favorit): UI sofort aktualisieren, Submit im Hintergrund.
React 19: useOptimistic
'use client'
import { useOptimistic } from 'react'
import { likePost } from '@/app/actions'
export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
const [optimisticLikes, setOptimisticLikes] = useOptimistic(initialLikes)
async function handleLike() {
// UI sofort aktualisieren (optimistisch)
setOptimisticLikes(optimisticLikes + 1)
// Submit im Hintergrund
await likePost(postId)
}
return (
<button onClick={handleLike}>
👍 {optimisticLikes}
</button>
)
}
Klick → Zahl steigt sofort, ohne auf den Server zu warten. Sehr flüssig.
Nur bei sehr hoher Erfolgsrate. Bei Fehlern muss die UI zurückgerollt werden – oft mehr Aufwand als Nutzen.
Fazit
Drei Kernpunkte:
-
Server Actions vereinfachen Formulare, sind aber nicht überall die Lösung. Interne Formulare: ja; externe APIs: Route Handlers. Nicht alles blind auf Server Actions umstellen.
-
Sicherheit liegt bei Ihnen. Das Framework liefert Grundschutz – Validierung, Auth und Autorisierung müssen Sie selbst implementieren. Next.js ersetzt keine Sicherheitsarchitektur.
-
UX-Details zählen. Loading, Fehlermeldungen, optimistische Updates – der Unterschied zwischen „geht so“ und „fühlt sich gut an“. Mit
useActionStateunduseFormStatussauber abbilden.
Starten Sie mit einem einfachen Formular: Server Action, Zod, Loading – damit haben Sie schon 80 % abgedeckt. Cache-Revalidierung und optimistische Updates können warten, bis Sie sie brauchen.
Next.js und React entwickeln sich schnell – Server-Actions-APIs können sich ändern. Offizielle Doku im Blick behalten.
Probieren Sie es im nächsten Projekt aus. Beim nächsten Formular-Submit merken Sie vielleicht: Es kann wirklich so einfach sein.
Vollständiger Workflow: Formulare mit Server Actions verarbeiten
Von der Server Action bis zu Validierung und State-Handling – Schritt für Schritt
⏱️ Estimated time: 30 min
- 1
Step 1: Server Action erstellen
Server Action in app/actions.ts anlegen:
1. Datei-Level-Markierung: 'use server' oben in der Datei
2. Funktion definieren: export async function actionName(formData: FormData)
3. Daten lesen: formData.get('fieldName') für Formularfelder
4. Ergebnis zurückgeben: Format { success: boolean, errors?: {}, message?: string }
Beispiel:
```typescript
'use server'
export async function signup(formData: FormData) {
const name = formData.get('name') as string
// Verarbeitungslogik...
return { success: true, message: 'Registrierung erfolgreich' }
}
``` - 2
Step 2: Zod-Validierung hinzufügen
Serverseitige Validierung mit Zod:
1. Zod installieren: npm install zod
2. Schema definieren: const SignupSchema = z.object({ name: z.string().min(2), email: z.string().email() })
3. Daten validieren: const result = SignupSchema.safeParse(rawData)
4. Fehler behandeln: if (!result.success) return { success: false, errors: result.error.flatten().fieldErrors }
Wichtig:
• safeParse wirft keine Exception, liefert { success, data/error }
• flatten().fieldErrors formatiert Fehler als { field: ['error1'] }
• Bei Validierungsfehlern strukturierte Fehler zurückgeben, damit der Client sie anzeigen kann - 3
Step 3: State mit useActionState verarbeiten
useActionState in Client-Komponenten:
1. Hook importieren: import { useActionState } from 'react'
2. Initialzustand: const initialState = { success: false, errors: {} }
3. Hook nutzen: const [state, formAction, isPending] = useActionState(action, initialState)
4. Formular binden: <form action={formAction}>
5. Fehler anzeigen: {state.errors?.field && <p>{state.errors.field[0]}</p>}
6. Loading anzeigen: <button disabled={isPending}>{isPending ? 'Wird gesendet...' : 'Absenden'}</button>
Ablauf:
• Nutzer sendet ab → Action wird aufgerufen → Ergebnis zurück → state aktualisiert → Komponente rendert neu - 4
Step 4: Authentifizierung und Autorisierung hinzufügen
Sicherheitsprüfungen in der Server Action:
1. Eingabevalidierung: alle Eingaben mit Zod prüfen
2. Authentifizierung: Session-Token prüfen
```typescript
const cookieStore = await cookies()
const sessionToken = cookieStore.get('session')?.value
if (!sessionToken) return { success: false, error: 'Bitte zuerst anmelden' }
```
3. Autorisierung: Berechtigung für die Aktion prüfen
```typescript
const post = await getPost(postId)
if (post.authorId !== currentUser.id) {
return { success: false, error: 'Keine Berechtigung' }
}
```
4. Aktion ausführen: nach erfolgreicher Prüfung die Geschäftslogik
Merken: Server Actions sind keine Magie – Sicherheitsprüfungen sind manuell nötig - 5
Step 5: Benutzererfahrung optimieren
Loading-Status und Fehlerbehandlung:
1. useFormStatus (in Button-Komponente):
```typescript
'use client'
import { useFormStatus } from 'react-dom'
export function SubmitButton() {
const { pending } = useFormStatus()
return <button disabled={pending}>...</button>
}
```
2. Cache mit revalidatePath aktualisieren:
```typescript
import { revalidatePath } from 'next/cache'
revalidatePath('/posts')
```
3. Optimistisches Update (optional, bei sehr hoher Erfolgsrate):
```typescript
const [optimisticState, setOptimisticState] = useOptimistic(initialState)
```
Best Practice:
• Komplexe Formularlogik → useActionState
• Unabhängige Button-Komponente → useFormStatus
• Nach erfolgreicher Aktion → relevanten Seiten-Cache aktualisieren
FAQ
Was ist der Unterschied zwischen Server Actions und API Routes? Wann nutze ich welche?
Sind Server Actions sicher? Welche Sicherheitsmaßnahmen sind nötig?
Was ist der Unterschied zwischen useActionState und useFormStatus?
Wie übergebe ich Parameter außerhalb der Formularfelder?
Wie aktualisiere ich Seitendaten nach dem Formular-Submit?
Wann sollte ich optimistische Updates nutzen?
12 Min. Lesezeit · Veröffentlicht am: 19. Dez. 2025 · Aktualisiert am: 14. Juli 2026
Next.js Komplettleitfaden
Wenn du über die Suche hier gelandet bist, kommst du am schnellsten weiter, indem du zum vorherigen oder nächsten Beitrag dieser Serie springst.
Vorheriger
Next.js SSR vs SSG vs ISR: Leitfaden zur Wahl der Rendering-Strategie
Unsicher, wann Sie in Next.js SSR, SSG oder ISR einsetzen? Dieser Artikel vergleicht Praxis-Szenarien und liefert einen Entscheidungsbaum – plus Lösungen für ISR-Konfiguration, die nicht greift, und langsame First-Paint-Ladezeiten.
Teil 9 von 51
Nächster
Next.js Middleware Praxisleitfaden: Pfad-Matching, Edge-Runtime-Einschränkungen und häufige Fallstricke
Vom Produktions-Bug zur vollständigen Lösung: Next.js Middleware matcher-Konfiguration, Edge-Runtime-Limits und drei Praxisszenarien – damit Sie die häufigsten Fallstricke vermeiden
Teil 11 von 51
Ähnliche Beiträge
Next.js App Router Einstieg: Kernkonzepte und Grundlagen im Überblick

Next.js App Router Einstieg: Kernkonzepte und Grundlagen im Überblick
Next.js 15 in der Praxis: Wie ich an einem Wochenende ein produktionsreifes Blog-System aufgebaut habe


Kommentare
Melde dich mit GitHub an, um einen Kommentar zu hinterlassen