patterntypescriptnextjsTip
Server Actions: 'use server' directive and form mutations
Viewed 0 times
Next.js 14+ (stable Server Actions), React 19 for useActionState
server actionsuse serverform actionmutationsrevalidatePathuseActionState
Error Messages
Problem
Handling form submissions in App Router requires either a separate API route or a Server Action. Using a traditional onSubmit with fetch works but loses progressive enhancement and adds boilerplate.
Solution
Define Server Actions with 'use server' and wire them to forms via the action prop:
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const body = formData.get('body') as string;
await db.post.create({ data: { title, body } });
revalidatePath('/posts');
redirect('/posts');
}
// app/new-post/page.tsx
import { createPost } from '../actions';
export default function NewPostPage() {
return (
<form action={createPost}>
<input name='title' required />
<textarea name='body' />
<button type='submit'>Create</button>
</form>
);
}
// With useActionState for pending/error state:
'use client';
import { useActionState } from 'react';
import { createPost } from '../actions';
export function NewPostForm() {
const [state, action, isPending] = useActionState(createPost, null);
return (
<form action={action}>
<input name='title' />
<button disabled={isPending}>Submit</button>
{state?.error && <p>{state.error}</p>}
</form>
);
}
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const body = formData.get('body') as string;
await db.post.create({ data: { title, body } });
revalidatePath('/posts');
redirect('/posts');
}
// app/new-post/page.tsx
import { createPost } from '../actions';
export default function NewPostPage() {
return (
<form action={createPost}>
<input name='title' required />
<textarea name='body' />
<button type='submit'>Create</button>
</form>
);
}
// With useActionState for pending/error state:
'use client';
import { useActionState } from 'react';
import { createPost } from '../actions';
export function NewPostForm() {
const [state, action, isPending] = useActionState(createPost, null);
return (
<form action={action}>
<input name='title' />
<button disabled={isPending}>Submit</button>
{state?.error && <p>{state.error}</p>}
</form>
);
}
Why
Server Actions run exclusively on the server, giving direct database/filesystem access without an API layer. They enable progressive enhancement — forms work even without JavaScript. Next.js handles CSRF protection automatically for Server Actions.
Gotchas
- 'use server' can be at the top of a file (all exports become actions) or inline inside a Server Component async function
- Server Actions cannot be defined in Client Component files — import them from a separate 'use server' file
- FormData values are always strings — coerce numbers explicitly: Number(formData.get('count'))
- useActionState is from React 19 — older React versions use useFormState from react-dom
Code Snippets
Simple Server Action with cache invalidation
'use server';
export async function deletePost(id: string) {
await db.post.delete({ where: { id } });
revalidatePath('/posts');
}Context
When handling form submissions and data mutations in Next.js App Router
Revisions (0)
No revisions yet.