Français

Un guide complet sur les Server Actions de Next.js 14, couvrant les bonnes pratiques de gestion des formulaires, la validation des données, la sécurité et les techniques avancées pour créer des applications web modernes.

Server Actions Next.js 14 : Maîtriser les bonnes pratiques de gestion des formulaires

Next.js 14 introduit des fonctionnalités puissantes pour créer des applications web performantes et conviviales. Parmi celles-ci, les Server Actions se distinguent comme une manière transformatrice de gérer les soumissions de formulaires et les mutations de données directement sur le serveur. Ce guide offre un aperçu complet des Server Actions dans Next.js 14, en se concentrant sur les bonnes pratiques pour la gestion des formulaires, la validation des données, la sécurité et les techniques avancées. Nous explorerons des exemples pratiques et fournirons des conseils concrets pour vous aider à construire des applications web robustes et évolutives.

Que sont les Server Actions de Next.js ?

Les Server Actions sont des fonctions asynchrones qui s'exécutent sur le serveur et peuvent être appelées directement depuis des composants React. Elles éliminent le besoin de routes d'API traditionnelles pour gérer les soumissions de formulaires et les mutations de données, ce qui se traduit par un code simplifié, une sécurité améliorée et des performances accrues. Les Server Actions sont des React Server Components (RSC), ce qui signifie qu'elles sont exécutées sur le serveur, entraînant des chargements de page initiaux plus rapides et un meilleur SEO.

Principaux avantages des Server Actions :

Configurer votre projet Next.js 14

Avant de plonger dans les Server Actions, assurez-vous d'avoir un projet Next.js 14 configuré. Si vous partez de zéro, créez un nouveau projet avec la commande suivante :

npx create-next-app@latest my-next-app

Assurez-vous que votre projet utilise la structure de répertoires app pour profiter pleinement des Server Components et des Actions.

Gestion de formulaire de base avec les Server Actions

Commençons par un exemple simple : un formulaire qui soumet des données pour créer un nouvel élément dans une base de données. Nous utiliserons un formulaire simple avec un champ de saisie et un bouton de soumission.

Exemple : Créer un nouvel élément

D'abord, définissez une fonction Server Action au sein de votre composant React. Cette fonction gérera la logique de soumission du formulaire sur le serveur.

// app/components/CreateItemForm.tsx
'use client';

import { useState } from 'react';

async function createItem(formData: FormData) {
  'use server'

  const name = formData.get('name') as string;

  // Simuler une interaction avec la base de données
  console.log('Création de l'élément :', name);

  await new Promise((resolve) => setTimeout(resolve, 1000)); // Simuler la latence

  console.log('Élément créé avec succès !');
}

export default function CreateItemForm() {
  const [isSubmitting, setIsSubmitting] = useState(false);
  
  async function handleSubmit(formData: FormData) {
    setIsSubmitting(true);
    await createItem(formData);
    setIsSubmitting(false);
  }

  return (
    
); }

Explication :

Validation des données

La validation des données est cruciale pour garantir leur intégrité et prévenir les vulnérabilités de sécurité. Les Server Actions offrent une excellente opportunité d'effectuer une validation côté serveur. Cette approche aide à atténuer les risques associés à la seule validation côté client.

Exemple : Valider les données d'entrée

Modifiez la Server Action createItem pour y inclure une logique de validation.

// app/components/CreateItemForm.tsx
'use client';

import { useState } from 'react';

async function createItem(formData: FormData) {
  'use server'

  const name = formData.get('name') as string;

  if (!name || name.length < 3) {
    throw new Error('Le nom de l\'élément doit comporter au moins 3 caractères.');
  }

  // Simuler une interaction avec la base de données
  console.log('Création de l\'élément :', name);

  await new Promise((resolve) => setTimeout(resolve, 1000)); // Simuler la latence

  console.log('Élément créé avec succès !');
}

export default function CreateItemForm() {
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [errorMessage, setErrorMessage] = useState(null);
  
  async function handleSubmit(formData: FormData) {
    setIsSubmitting(true);
    setErrorMessage(null);
    try {
      await createItem(formData);
    } catch (error: any) {
      setErrorMessage(error.message || 'Une erreur est survenue.');
    } finally {
      setIsSubmitting(false);
    }
  }

  return (
    
{errorMessage &&

{errorMessage}

}
); }

Explication :

Utiliser des bibliothèques de validation

Pour des scénarios de validation plus complexes, envisagez d'utiliser des bibliothèques de validation comme :

Voici un exemple avec Zod :

// app/utils/validation.ts
import { z } from 'zod';

export const CreateItemSchema = z.object({
  name: z.string().min(3, 'Le nom de l\'élément doit comporter au moins 3 caractères.'),
});
// app/components/CreateItemForm.tsx
'use client';

import { useState } from 'react';
import { CreateItemSchema } from '../utils/validation';

async function createItem(formData: FormData) {
  'use server'

  const name = formData.get('name') as string;

  const validatedFields = CreateItemSchema.safeParse({ name });

  if (!validatedFields.success) {
    return { errors: validatedFields.error.flatten().fieldErrors };
  }

  // Simuler une interaction avec la base de données
  console.log('Création de l\'élément :', name);

  await new Promise((resolve) => setTimeout(resolve, 1000)); // Simuler la latence

  console.log('Élément créé avec succès !');
}

export default function CreateItemForm() {
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [errorMessage, setErrorMessage] = useState(null);
  
  async function handleSubmit(formData: FormData) {
    setIsSubmitting(true);
    setErrorMessage(null);
    try {
      await createItem(formData);
    } catch (error: any) {
      setErrorMessage(error.message || 'Une erreur est survenue.');
    } finally {
      setIsSubmitting(false);
    }
  }

  return (
    
{errorMessage &&

{errorMessage}

}
); }

Explication :

Considérations de sécurité

Les Server Actions améliorent la sécurité en exécutant le code sur le serveur, mais il est toujours crucial de suivre les bonnes pratiques de sécurité pour protéger votre application contre les menaces courantes.

Prévenir les attaques de type Cross-Site Request Forgery (CSRF)

Les attaques CSRF exploitent la confiance qu'un site web accorde au navigateur d'un utilisateur. Pour prévenir les attaques CSRF, mettez en œuvre des mécanismes de protection CSRF.

Next.js gère automatiquement la protection CSRF lors de l'utilisation des Server Actions. Le framework génère et valide un jeton CSRF pour chaque soumission de formulaire, garantissant que la requête provient bien de votre application.

Gérer l'authentification et l'autorisation des utilisateurs

Assurez-vous que seuls les utilisateurs autorisés peuvent effectuer certaines actions. Mettez en œuvre des mécanismes d'authentification et d'autorisation pour protéger les données et fonctionnalités sensibles.

Voici un exemple utilisant NextAuth.js pour protéger une Server Action :

// app/components/CreateItemForm.tsx
'use client';

import { useState } from 'react';
import { getServerSession } from 'next-auth';
import { authOptions } from '../../app/api/auth/[...nextauth]/route';

async function createItem(formData: FormData) {
  'use server'

  const session = await getServerSession(authOptions);

  if (!session) {
    throw new Error('Non autorisé');
  }

  const name = formData.get('name') as string;

  // Simuler une interaction avec la base de données
  console.log('Création de l\'élément :', name, 'par l\'utilisateur :', session.user?.email);

  await new Promise((resolve) => setTimeout(resolve, 1000)); // Simuler la latence

  console.log('Élément créé avec succès !');
}

export default function CreateItemForm() {
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [errorMessage, setErrorMessage] = useState(null);
  
  async function handleSubmit(formData: FormData) {
    setIsSubmitting(true);
    setErrorMessage(null);
    try {
      await createItem(formData);
    } catch (error: any) {
      setErrorMessage(error.message || 'Une erreur est survenue.');
    } finally {
      setIsSubmitting(false);
    }
  }

  return (
    
{errorMessage &&

{errorMessage}

}
); }

Explication :

Nettoyer les données d'entrée

Nettoyez les données d'entrée pour prévenir les attaques de type Cross-Site Scripting (XSS). Les attaques XSS se produisent lorsque du code malveillant est injecté dans un site web, compromettant potentiellement les données des utilisateurs ou les fonctionnalités de l'application.

Utilisez des bibliothèques comme DOMPurify ou sanitize-html pour nettoyer les entrées fournies par l'utilisateur avant de les traiter dans vos Server Actions.

Techniques avancées

Maintenant que nous avons couvert les bases, explorons quelques techniques avancées pour utiliser efficacement les Server Actions.

Mises à jour optimistes

Les mises à jour optimistes offrent une meilleure expérience utilisateur en mettant immédiatement à jour l'interface utilisateur comme si l'action allait réussir, avant même que le serveur ne le confirme. Si l'action échoue sur le serveur, l'interface utilisateur est restaurée à son état précédent.

// app/components/UpdateItemForm.tsx
'use client';

import { useState } from 'react';

async function updateItem(id: string, formData: FormData) {
  'use server'

  const name = formData.get('name') as string;

  // Simuler une interaction avec la base de données
  console.log('Mise à jour de l\'élément :', id, 'avec le nom :', name);

  await new Promise((resolve) => setTimeout(resolve, 1000)); // Simuler la latence

  // Simuler un échec (à des fins de démonstration)
  const shouldFail = Math.random() < 0.5;
  if (shouldFail) {
    throw new Error('Échec de la mise à jour de l\'élément.');
  }

  console.log('Élément mis à jour avec succès !');
  return { name }; // Renvoyer le nom mis à jour
}

export default function UpdateItemForm({ id, initialName }: { id: string; initialName: string }) {
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [errorMessage, setErrorMessage] = useState(null);
  const [itemName, setItemName] = useState(initialName);

  async function handleSubmit(formData: FormData) {
    setIsSubmitting(true);
    setErrorMessage(null);

    // Mettre à jour l'UI de manière optimiste
    const newName = formData.get('name') as string;
    setItemName(newName);

    try {
      const result = await updateItem(id, formData);
      // En cas de succès, la mise à jour est déjà reflétée dans l'UI via setItemName

    } catch (error: any) {
      setErrorMessage(error.message || 'Une erreur est survenue.');
      // Rétablir l'UI en cas d'erreur
      setItemName(initialName);
    } finally {
      setIsSubmitting(false);
    }
  }

  return (
    

Nom actuel : {itemName}

{errorMessage &&

{errorMessage}

}
); }

Explication :

Revalidation des données

Après qu'une Server Action a modifié des données, vous devrez peut-être revalider les données mises en cache pour vous assurer que l'interface utilisateur reflète les derniers changements. Next.js offre plusieurs moyens de revalider les données :

Voici un exemple de revalidation d'un chemin après la création d'un nouvel élément :

// app/components/CreateItemForm.tsx
'use client';

import { useState } from 'react';
import { revalidatePath } from 'next/cache';

async function createItem(formData: FormData) {
  'use server'

  const name = formData.get('name') as string;

  // Simuler une interaction avec la base de données
  console.log('Création de l\'élément :', name);

  await new Promise((resolve) => setTimeout(resolve, 1000)); // Simuler la latence

  console.log('Élément créé avec succès !');

  revalidatePath('/items'); // Revalider le chemin /items
}

export default function CreateItemForm() {
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [errorMessage, setErrorMessage] = useState(null);
  
  async function handleSubmit(formData: FormData) {
    setIsSubmitting(true);
    setErrorMessage(null);
    try {
      await createItem(formData);
    } catch (error: any) {
      setErrorMessage(error.message || 'Une erreur est survenue.');
    } finally {
      setIsSubmitting(false);
    }
  }

  return (
    
{errorMessage &&

{errorMessage}

}
); }

Explication :

Bonnes pratiques pour les Server Actions

Pour maximiser les avantages des Server Actions, tenez compte des bonnes pratiques suivantes :

Pièges courants et comment les éviter

Bien que les Server Actions offrent de nombreux avantages, il existe certains pièges courants à connaître :

Conclusion

Les Server Actions de Next.js 14 offrent un moyen puissant et efficace de gérer les soumissions de formulaires et les mutations de données directement sur le serveur. En suivant les bonnes pratiques décrites dans ce guide, vous pouvez créer des applications web robustes, sécurisées et performantes. Adoptez les Server Actions pour simplifier votre code, renforcer la sécurité et améliorer l'expérience utilisateur globale. En intégrant ces principes, tenez compte de l'impact mondial de vos choix de développement. Assurez-vous que vos formulaires et vos processus de gestion des données sont accessibles, sécurisés et conviviaux pour des publics internationaux variés. Cet engagement en faveur de l'inclusivité améliorera non seulement l'utilisabilité de votre application, mais élargira également sa portée et son efficacité à l'échelle mondiale.