---
name: nextjs-server-actions
description: "Server Actions done right — form mutations, optimistic UI, useOptimistic, revalidatePath/Tag. Production Server Actions: `'use server'` boundary, form action vs imperative call, useFormState + useFormStatus, useOptimistic for instant UI, revalidatePath vs revalidateTag, and the error-handling pattern that doesn't leak server stack traces."
license: MIT
---

# Next.js Server Actions

A focused skill for building data mutations in the Next.js App Router the way they're meant to work: async functions marked `'use server'`, wired to forms or called imperatively, with progressive enhancement, optimistic UI, and cache revalidation. It's for developers who've gotten Server Actions running but keep hitting the rough edges — stale lists after a write, double-submits, leaked error details, and the `useFormState`/`useOptimistic` mental model.

## When to use it

- A form submits, the row saves, but the list above it doesn't update until a refresh.
- You want a "Like" or "Add to cart" button to flip instantly, then reconcile with the server.
- You need a mutation to work even before JavaScript hydrates (a real `<form action={...}>`).
- You're returning `{ error: e.message }` and worried about what that leaks in production.
- You can't decide between `revalidatePath` and `revalidateTag`.

## How it works

The skill covers the actual primitives. Define actions either inline in a Server Component or in a `'use server'` file (every export becomes a callable endpoint — validate inputs accordingly). Bind them with `<form action={createTodo}>` for progressive enhancement, or call them imperatively inside `startTransition`. For form state and validation messages, use `useActionState` (the current name; `useFormState` from `react-dom` is the older alias). Disable submit buttons with `useFormStatus`'s `pending`. Drive instant feedback with `useOptimistic`. After a write, call `revalidatePath('/todos')` to refresh a route or `revalidateTag('todos')` to invalidate everything fetched with that cache tag. Use `redirect()` to navigate post-mutation, and `cookies()`/`headers()` for auth inside the action.

## Example

```ts
// app/todos/actions.ts
'use server'
import { revalidateTag } from 'next/cache'
import { z } from 'zod'

const Schema = z.object({ title: z.string().min(1) })

export async function createTodo(_prev: unknown, formData: FormData) {
  const parsed = Schema.safeParse({ title: formData.get('title') })
  if (!parsed.success) return { error: 'Title is required' }
  try {
    await db.todo.create({ data: parsed.data })
    revalidateTag('todos')
    return { ok: true }
  } catch {
    return { error: 'Could not save. Try again.' } // never expose e.message
  }
}
```

## Tips & gotchas

- Every export from a `'use server'` file is a public POST endpoint. Authorize and validate inside each one — never trust the client.
- `useActionState` returns `[state, formAction, isPending]`; pass `formAction` to `<form action>`, not the raw action.
- `revalidatePath`/`revalidateTag` only work if your `fetch`/data layer is actually cached and tagged; uncached reads ignore them.
- Don't `throw` raw errors to the client — return a typed result object so stack traces stay server-side.

## FAQ

**`revalidatePath` vs `revalidateTag`?** Path invalidates one route's cache; tag invalidates every cached fetch carrying that tag, across routes. Tags scale better for shared data.

**Can actions run on GET?** No — they're POST-only. Use route handlers or Server Component reads for fetching.

**Why is my optimistic update flickering?** You likely mutate state outside `startTransition`, so React reverts before the action resolves. Wrap the call.
