Aller au contenu

Server Actions et formulaires

Qu'est-ce qu'une Server Action

Les Server Actions sont des fonctions asynchrones qui s'exécutent sur le serveur et peuvent être appelées directement depuis vos composants. Elles permettent de gérer les soumissions de formulaires et les mutations de données sans créer de route API.

La directive "use server"

Pour déclarer une Server Action, ajoutez la directive "use server" au début du fichier :

app/actions/produit.actions.ts
"use server";

import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache";

export async function creerProduit(formData: FormData) {
  const nom = formData.get("nom") as string;
  const description = formData.get("description") as string;
  const prix = parseFloat(formData.get("prix") as string);
  const categorieId = parseInt(formData.get("categorieId") as string);

  // Validation
  if (!nom || !prix || !categorieId) {
    return { erreur: "Tous les champs obligatoires doivent être remplis." };
  }

  await prisma.produit.create({
    data: { nom, description, prix, categorieId },
  });

  revalidatePath("/produits");
}

export async function supprimerProduit(id: number) {
  await prisma.produit.delete({
    where: { id },
  });

  revalidatePath("/produits");
}

Points importants :

  • "use server" indique que toutes les fonctions exportées de ce fichier sont des Server Actions.
  • Ces fonctions s'exécutent uniquement sur le serveur, jamais dans le navigateur.
  • revalidatePath permet de rafraîchir les données affichées sur une page après une modification.

Formulaires avec Server Actions

Les Server Actions peuvent être utilisées directement dans l'attribut action d'un formulaire HTML :

app/produits/nouveau/page.tsx
import { creerProduit } from "@/app/actions/produit.actions";

export default function NouveauProduit() {
  return (
    <main>
      <h1>Ajouter un produit</h1>

      <form action={creerProduit}>
        <div>
          <label htmlFor="nom">Nom :</label>
          <input type="text" id="nom" name="nom" required />
        </div>

        <div>
          <label htmlFor="description">Description :</label>
          <textarea id="description" name="description" />
        </div>

        <div>
          <label htmlFor="prix">Prix :</label>
          <input type="number" id="prix" name="prix" step="0.01" required />
        </div>

        <div>
          <label htmlFor="categorieId">Catégorie (ID) :</label>
          <input type="number" id="categorieId" name="categorieId" required />
        </div>

        <button type="submit">Ajouter</button>
      </form>
    </main>
  );
}

Lorsque le formulaire est soumis, la fonction creerProduit est appelée sur le serveur avec les données du formulaire sous forme de FormData.

Utiliser une Server Action avec bind

Pour passer des arguments supplémentaires à une Server Action (comme un ID), utilisez .bind() :

app/produits/page.tsx
import Link from "next/link";
import { prisma } from "@/lib/prisma";
import { supprimerProduit } from "@/app/actions/produit.actions";

export default async function ProduitsPage() {
  const produits = await prisma.produit.findMany({
    include: { categorie: true },
  });

  return (
    <main>
      <h1>Liste des produits</h1>
      <Link href="/produits/nouveau">Ajouter un produit</Link>

      <table>
        <thead>
          <tr>
            <th>Nom</th>
            <th>Prix</th>
            <th>Catégorie</th>
            <th>Actions</th>
          </tr>
        </thead>
        <tbody>
          {produits.map((produit) => (
            <tr key={produit.id}>
              <td>{produit.nom}</td>
              <td>{produit.prix} $</td>
              <td>{produit.categorie.nom}</td>
              <td>
                <form action={supprimerProduit.bind(null, produit.id)}>
                  <button type="submit">Supprimer</button>
                </form>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </main>
  );
}

Dans cet exemple, supprimerProduit.bind(null, produit.id) crée une nouvelle fonction qui appellera supprimerProduit avec l'ID du produit en premier argument.

Validation de formulaires

Il est important de valider les données côté serveur dans vos Server Actions. Voici un exemple avec validation et retour d'erreurs :

Validation dans une Server Action
"use server";

import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache";

interface ResultatAction {
  erreur?: string;
  succes?: boolean;
}

export async function creerProduit(
  etatPrecedent: ResultatAction,
  formData: FormData
): Promise<ResultatAction> {
  const nom = formData.get("nom") as string;
  const prix = parseFloat(formData.get("prix") as string);

  // Validation
  if (!nom || nom.trim().length === 0) {
    return { erreur: "Le nom est requis." };
  }

  if (isNaN(prix) || prix <= 0) {
    return { erreur: "Le prix doit être un nombre positif." };
  }

  await prisma.produit.create({
    data: { nom, prix, categorieId: 1 },
  });

  revalidatePath("/produits");
  return { succes: true };
}

Pour utiliser cette version avec validation, le formulaire doit être un Client Component utilisant le hook useActionState :

Formulaire avec validation (Client Component)
"use client";

import { useActionState } from "react";
import { creerProduit } from "@/app/actions/produit.actions";

export default function FormulaireProduit() {
  const [etat, action, enAttente] = useActionState(creerProduit, {});

  return (
    <form action={action}>
      <div>
        <label htmlFor="nom">Nom :</label>
        <input type="text" id="nom" name="nom" required />
      </div>

      <div>
        <label htmlFor="prix">Prix :</label>
        <input type="number" id="prix" name="prix" step="0.01" required />
      </div>

      {etat.erreur && <p style={{ color: "red" }}>{etat.erreur}</p>}
      {etat.succes && <p style={{ color: "green" }}>Produit ajouté!</p>}

      <button type="submit" disabled={enAttente}>
        {enAttente ? "Ajout en cours..." : "Ajouter"}
      </button>
    </form>
  );
}

Revalidation des données

Après une mutation (création, modification, suppression), il faut indiquer à Next.js de rafraîchir les données affichées. La fonction revalidatePath permet de revalider les données associées à un chemin :

Revalidation
import { revalidatePath } from "next/cache";

// Revalider une page spécifique
revalidatePath("/produits");

// Revalider toutes les pages
revalidatePath("/", "layout");

Sans revalidation, les pages afficheront les données en cache et ne refléteront pas les modifications effectuées par la Server Action.