Server actions Next.js : formulaires et interactions

Publié le 25/06/2024 - Ecrit par Antoine Bourin

6 minutes

Ajoutées lors de la release de Next.js 14, les server actions (ou actions serveur) vous permettent de gérer  toutes les interactions des utilisateurs côté serveur ! Cela vous permet d'interagir avec votre base de données sans passer par une nouvelle route API, garder des données sensibles côté serveur, simplifier la gestion d'état d'un formulaire.


Nous allons prendre un exemple concret pour comprendre les changements majeurs que cette fonctionnalité apporte.

Exemple : créer et dynamiser un formulaire d'inscription

Prenons ensemble l'exemple de création d'un formulaire d'inscription pour constater les changements apportés par les server actions.

Voici dans notre cas ce que pourrait représenter la dynamisation de notre formulaire avec et sans action serveur :

Schéma des server actions avec Next.js

Sans l'utilisation de server actions

Jusqu'ici, sans l'utilisation de server actions, pour dynamiser un formulaire d'inscription par exemple nous avions besoin de : 

  1. Transformer le composant formulaire en client avec la directive "use client".
  2. Attacher un événement onSubmit au formulaire.
  3. Créer une route back-end (via route handlers dans Next.js ou autre api).
  4. Appeler via fetch la route API créée.

Le code de notre composant pourrait ressembler à ça :

"use client";

const SignupForm = () => {
  const [error, setError] = useState(null);
  const [success, setSuccess] = useState(null);

  const createUser = async (e) => {
    e.preventDefault();
    // récupération des valeurs
    try {
      const response = await fetch("https://....");
    } catch (error) {
      setError("unexpected error");
    }
  }

  return (
    <form>
      {/** .... **/}
      <button onClick={createUser}>Créer mon compte</button>
      {error && <p>{error}</p>}
    </form>
  );
}

export default SignupForm;

Il faudrait encore créer la route API dédiée. La mise en place de tout ce code peut être assez pénible.

Avec l'utilisation de server actions

Cette fonctionnalité peut simplifier énormément la gestion d'un formulaire comme celui-ci. L'ensemble de la dynamisation et des traitements seront effectués côté serveur. Maintenant nous aurons seulement besoin de :

  1. Attacher au formulaire, à la balise action, une fonction asynchrone.
  2. Effectuer n'importe quel traitement nécessaire dans la fonction créée (modification de la base de données, revalidation des caches, mutation de données, redirections...).

L'ensemble du code ici reste traité côté serveur. Le composant formulaire ne nécessite plus d'être passé en client, aucun appel supplémentaire est effectué.

Toutes les données du formulaire sont récupérées en paramètre de la fonction sous forme d'objet FormData. Vous n'avez plus à gérer l'événement directement.

Du fait que le contexte d'exécution des actions serveurs soit serveur, vous gardez aussi en sécurité toute les informations que vous utilisez (token, clés secrètes, informations sensibles). Vous pouvez également en profiter pour interagir avec des ressources back-end et utiliser Prisma par exemple.

La fonction créée nécessite d'être préfixée par la directive "use server" pour être traitée comme server action.

const SignupForm = () => {

  async function createUser(formData: FormData) {
    'use server'
 
    const newUser = {
      username: formData.get('username'),
      address: formData.get('address'),
      email: formData.get('email'),
    }
 
    // création de l'utilisateur
    // revalidation du cache (revalidatePath, revalidateTag...)
    // redirect, cookies...
  }
 
  return <form action={createUser}>...</form>
}

export default SignupForm;

Cette fonction peut également être exportée depuis un fichier externe au composant. Dans ce cas, le fichier devra présenter la directive "use server" en première ligne.

Une idée ? Un projet ? Besoin d'aide ?

Trouvez un créneau dans mon calendrier et échangeons simplement.

Erreurs et validation avec les server actions

Validation des données du formulaire et hook useFormState

Pour valider les données de votre formulaire, vous pourriez utiliser dans la fonction une librairie de validation tel que zod, joi ou yup.

L'utilisation du hook useFormState vous permet de gérer facilement l'état du formulaire, le retour de l'action serveur et ainsi modifier l'interface en conséquence.

Ici, un nouvel exemple de notre formulaire de création d'un utilisateur avec une gestion d'état :

Nous pouvons choisir de renvoyer un message à la fin de l'exécution de notre server action. Celui-ci sera utilisé comme state grâce au hook useFormState.

'use server'
 
export async function createUser(prevState: any, formData: FormData) {
  // ...
  return {
    message: 'Please enter a valid email',
  }
}

Maintenant, insérer la fonction en paramètre du hook ainsi que l'état initial (comme on le ferait pour le hook useState) :

'use client'
 
import { useFormState } from 'react-dom'
import { createUser } from '@/app/actions'
 
const initialState = {
  message: '',
}
 
const SignupForm = () => {
  const [state, formAction] = useFormState(createUser, initialState)
 
  return (
    <form action={formAction}>
      <label htmlFor="email">Email</label>
      <input type="text" id="email" name="email" required />
      {/* ... */}
      <p>
        {state?.message}
      </p>
      <button>Sign up</button>
    </form>
  )
}

export default SignupForm;

Notez que ceci nécessite que notre composant devienne un composant client de nouveau. Dans le cas d'utilisation d'actions serveur au sein d'un composant client, l'action doit obligatoirement avoir été définie dans un fichier séparé précédé de la directive use server.

Traitement des erreurs

Pour les erreurs qui surviendraient au cours de la mutation des données ou de l'utilisation des ressources back-end, vous pourriez déclencher une erreur qui serait interceptée par l'Error boundary défini le plus proche (par la création d'un fichier error.js) :

'use server'
 
export async function createUser(formData: FormData) {
  try {
    // Mutation de données
  } catch (e) {
    throw new Error('Failed to create user')
  }
}

Vous pouvez également choisir de renvoyer un message d'erreur lorsque vous utilisez le hook useFormState.

Server actions sur d'autres éléments du DOM

Vous pouvez appliquer des server actions sur d'autres éléments que des formulaires comme des boutons, des input ou encore directement à l'intérieur de useEffect.

Ceci permettrait de conserver des traitements côté serveur sans avoir à créer une nouvelle route API, faire un nouvel appel fetch etc.

Exemple : interaction avec un bouton compteur de j'aime

Commençons par créer l'action serveur, dans un fichier distinct :

"use server"

// ...

export async function addLike() {
  const likes = await db.incrementLike();

  return likes;
}

Nous pouvons maintenant l'appeler directement depuis le composant, sur l'événement onClick :

'use client'
 
import { addLike } from './actions'
import { useState } from 'react'
 
const LikeButton = ({ initialLikes }) => {
  const [likes, setLikes] = useState(initialLikes)
 
  return (
    <button
      onClick={async () => {
        const updatedLikes = await addLike()
        setLikes(updatedLikes)
      }}
    >
      {likes} J'aime
    </button>
  )
}

export default LikeButton;

Pour conclure...

Vous avez beaucoup d'intérêt à utiliser les actions serveurs pour vos formulaires et autres éléments dans votre application.

D'une part, le fait que l'environnement de cette fonction soit exécuté côté serveur vous permet d'interagir directement avec des éléments back-end (mise à jour de base de données, mise à jour du cache, redirections, utilisation des cookies...). D'autre part, l'ensemble des hooks relatifs à cette nouvelle fonctionnalité vous permet de gérer plus facilement l'état final de votre interaction (useFormState, useFormStatus...).

Envie d'en apprendre plus sur Next.js ?

Découvrez une formation Next.js complète :

  • Comprendre et créer un système de routage complet
  • Créer des routes statiques et dynamique
  • Maîtriser le rendu serveur et client
  • Utiliser tous les atouts majeurs proposés par le framework

Plus de 35 heures de contenu vidéo

Plus de 80 cours écrits