Qu’est-ce qu’une Server Action ?
Une Server Action est une fonction async qui s’exécute exclusivement côté serveur, mais que vous appelez comme une fonction JavaScript normale depuis un composant client. Fini les routes API /api/... pour les mutations — une Server Action remplace tout ça avec bien moins de code.
// app/api/posts/route.ts
export async function POST(req: Request) {
const body = await req.json();
await db.posts.create(body);
return Response.json({ ok: true });
}
// Composant client
await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify(data),
});// app/actions/posts.ts
'use server';
export async function createPost(data) {
await db.posts.create(data);
}
// Composant client
await createPost(data);
// C'est tout.Créer une Server Action
Deux façons de déclarer une Server Action : avec "use server" en tête de fichier (toutes les fonctions du fichier deviennent des actions), ou avec la directive inline dans une fonction.
'use server'; // Tout ce fichier = Server Actions
import { revalidateTag, updateTag } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
if (!title || !content) {
return { error: 'Titre et contenu requis' };
}
await db.posts.create({ title, content });
revalidateTag('posts', 'max'); // invalide le cache
redirect('/blog'); // redirige après succès
}
export async function deletePost(id: string) {
await db.posts.delete(id);
updateTag(`post-${id}`); // invalidation immédiate
}
Formulaire avec Server Action
La façon la plus simple : passer la Server Action directement à l’attribut action d’un <form>. Ça fonctionne même sans JavaScript activé dans le navigateur.
import { createPost } from '@/app/actions/posts';
export default function NewPostPage() {
return (
<form action={createPost} className="max-w-lg mx-auto space-y-4 py-12">
<div>
<label className="block text-sm font-medium mb-1">Titre</label>
<input
name="title"
required
className="w-full border rounded-xl px-4 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Contenu</label>
<textarea
name="content"
rows={6}
required
className="w-full border rounded-xl px-4 py-2"
/>
</div>
<button
type="submit"
className="bg-gray-900 text-white px-6 py-3 rounded-xl font-semibold hover:bg-gray-700"
>
Publier
</button>
</form>
);
}
État pending avec useActionState
Pour afficher un état de chargement et les erreurs de retour, utilisez le hook useActionState dans un Client Component :
'use client';
import { useActionState } from 'react';
import { createPost } from '@/app/actions/posts';
export default function PostForm() {
const [state, action, isPending] = useActionState(createPost, null);
return (
<form action={action} className="space-y-4">
{state?.error && (
<p className="text-red-600 text-sm bg-red-50 p-3 rounded-xl">
{state.error}
</p>
)}
<input name="title" placeholder="Titre" className="w-full border rounded-xl px-4 py-2" />
<textarea name="content" placeholder="Contenu" className="w-full border rounded-xl px-4 py-2" />
<button
type="submit"
disabled={isPending}
className="bg-gray-900 text-white px-6 py-3 rounded-xl disabled:opacity-50"
>
{isPending ? 'Publication...' : 'Publier'}
</button>
</form>
);
}
refresh() est une nouvelle Server Action-only API de Next.js 16. Elle rafraîchit les données dynamiques affichées ailleurs sur la page (compteurs, notifications…) sans toucher au cache. Complémentaire à router.refresh() côté client.
Bouton de suppression avec Server Action inline
import { deletePost } from '@/app/actions/posts';
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPost(slug);
return (
<article>
<h1>{post.title}</h1>
{/* form + action = bouton de suppression sans JS */}
<form
action={async () => {
'use server'; // directive inline
await deletePost(post.id);
}}
>
<button
type="submit"
className="text-red-600 text-sm hover:underline"
>
Supprimer
</button>
</form>
</article>
);
}
Exercice pratique
- Créez un fichier
app/actions/todos.tsavec"use server"et deux fonctions :addTodo(formData)etdeleteTodo(id) - Créez
app/todos/page.tsx(Server Component) qui liste les todos et inclut un formulaire utilisantaddTodo - Ajoutez
revalidateTag('todos', 'max')après chaque mutation - Créez un composant
TodoForm.tsx(Client Component) avecuseActionStatepour afficher l’état pending et les erreurs - Testez la soumission avec JavaScript désactivé dans le navigateur — le formulaire doit fonctionner quand même