Server Actions et formulaires¶
Qu'est-ce qu'une Server Action¶
En React pur, pour interagir avec une base de données (ex: un formulaire pour l'ajout d'un produit), il faut appeler un API (souvent en Express ou PHP). Les actions serveurs élimine le besoin d'un API distinct en permettant l'appel de fonctions asynchrone entre la portion client et serveur de votre application.
"use server"¶
Une action serveur doit avoir "use server" au début du fichier :
'use server';
import { prisma } from '@/lib/prisma';
import { revalidatePath } from 'next/cache';
export async function creerProduit(
prevState: { erreur: string } | undefined,
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');
}
Formulaires avec Server Actions¶
On peut utiliser une actions serveur directement dans l'attribut action d'un formulaire HTML :
import { creerProduit } from '@/app/actions/produit.actions';
import { useActionState } from 'react';
export default function NouveauProduit() {
const [state, formAction, pending] = useActionState(creerProduit, undefined);
return (
<main>
<h1>Ajouter un produit</h1>
<form action={formAction}>
<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>
{state?.erreur && (
<p
aria-live="polite"
className="rounded-lg bg-red-50 px-3 py-2 text-sm text-red-700 dark:bg-red-950/40 dark:text-red-400"
>
{state.erreur}
</p>
)}
<button type="submit" disabled={pending}>
{pending ? 'Ajout en cours…' : 'Ajouter'}
</button>
</form>
</main>
);
}
Lorsque le formulaire est soumis, la fonction creerProduit est appelée sur le serveur avec les données du formulaire en FormData.
Utiliser une Server Action avec bind¶
Si vous avez des variables à passer à l'action serveur sans l'ajouter comme champ du formulaire, il faut utiliser .bind() :
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>
);
}
Validation de formulaires¶
Même si la validation est faite du côté client, pour des raisons de sécurité et d'intégrité il faut valider toutes les données du formulaire dans l'action serveur.
"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 :
"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 un changement dans les données (création, modification, suppression), il faut dire à Next.js de rafraîchir les données affichées en utilisant revalidatePath :
import { revalidatePath } from "next/cache";
// Revalider une page spécifique
revalidatePath("/produits");
// Revalider toutes les pages
revalidatePath("/", "layout");
Sans revalidatePath, les pages afficheront les données en cache sans les dernières modifications...