Leçon 6 / 9
67%

Next.js 16 – Leçon 6 : Server Actions et mutations

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.

Avant : Route API + fetch
// 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), });
Maintenant : Server Action
// 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.

TypeScript app/actions/posts.ts
'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.

TypeScript app/blog/new/page.tsx
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 :

TypeScript app/components/PostForm.tsx
'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() — rafraîchir les données non cachées

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

TypeScript app/blog/[slug]/page.tsx
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.ts avec "use server" et deux fonctions : addTodo(formData) et deleteTodo(id)
  • Créez app/todos/page.tsx (Server Component) qui liste les todos et inclut un formulaire utilisant addTodo
  • Ajoutez revalidateTag('todos', 'max') après chaque mutation
  • Créez un composant TodoForm.tsx (Client Component) avec useActionState pour afficher l’état pending et les erreurs
  • Testez la soumission avec JavaScript désactivé dans le navigateur — le formulaire doit fonctionner quand même