Actions serveur et mutations

Les actions serveur (Server Actions) sont des fonctions asynchrones exécutées sur le serveur. Elles peuvent être appelées dans les composants serveur et client pour gérer les soumissions de formulaires et les mutations de données dans les applications Next.js.

🎥 À regarder : En savoir plus sur les mutations avec les actions serveur → YouTube (10 minutes).

Convention

Une action serveur peut être définie avec la directive React "use server". Vous pouvez placer la directive en haut d'une fonction async pour marquer la fonction comme une action serveur, ou en haut d'un fichier séparé pour marquer toutes les exportations de ce fichier comme des actions serveur.

Composants serveur

Les composants serveur peuvent utiliser la directive "use server" au niveau de la fonction ou du module. Pour intégrer une action serveur, ajoutez "use server" en haut du corps de la fonction :

export default function Page() {
  // Action serveur
  async function create() {
    'use server'
    // Mutation de données
  }

  return '...'
}

Composants client

Pour appeler une fonction serveur dans un composant client, créez un nouveau fichier et ajoutez la directive "use server" en haut. Toutes les fonctions exportées dans le fichier seront marquées comme des fonctions serveur pouvant être réutilisées dans les composants client et serveur :

'use server'

export async function create() {}

Passage d'actions comme props

Vous pouvez également passer une action serveur à un composant client comme prop :

<ClientComponent updateItemAction={updateItem} />
'use client'

export default function ClientComponent({
  updateItemAction,
}: {
  updateItemAction: (formData: FormData) => void
}) {
  return <form action={updateItemAction}>{/* ... */}</form>
}

Habituellement, le plugin TypeScript de Next.js signalerait updateItemAction dans client-component.tsx car il s'agit d'une fonction qui ne peut généralement pas être sérialisée entre les limites client-serveur. Cependant, les props nommées action ou se terminant par Action sont supposées recevoir des actions serveur. Ceci n'est qu'une heuristique car le plugin TypeScript ne sait pas réellement si elle reçoit une action serveur ou une fonction ordinaire. La vérification de type à l'exécution garantira toujours que vous ne passiez pas accidentellement une fonction à un composant client.

Comportement

  • Les actions serveur peuvent être invoquées en utilisant l'attribut action dans un élément <form>.
    • Les composants serveur prennent en charge l'amélioration progressive par défaut, ce qui signifie que le formulaire sera soumis même si JavaScript n'est pas encore chargé ou est désactivé.
    • Dans les composants client, les formulaires invoquant des actions serveur mettront en file d'attente les soumissions si JavaScript n'est pas encore chargé, en priorisant l'hydratation client.
    • Après l'hydratation, le navigateur ne se rafraîchit pas lors de la soumission du formulaire.
  • Les actions serveur ne sont pas limitées aux <form> et peuvent être invoquées depuis des gestionnaires d'événements, useEffect, des bibliothèques tierces et d'autres éléments de formulaire comme <button>.
  • Les actions serveur s'intègrent à l'architecture de mise en cache et revalidation de Next.js. Lorsqu'une action est invoquée, Next.js peut renvoyer à la fois l'interface utilisateur mise à jour et les nouvelles données en un seul aller-retour serveur.
  • En arrière-plan, les actions utilisent la méthode POST, et seule cette méthode HTTP peut les invoquer.
  • Les arguments et la valeur de retour des actions serveur doivent être sérialisables par React. Voir la documentation React pour une liste des arguments et valeurs sérialisables.
  • Les actions serveur sont des fonctions. Cela signifie qu'elles peuvent être réutilisées n'importe où dans votre application.
  • Les actions serveur héritent du runtime de la page ou du layout dans lequel elles sont utilisées.
  • Les actions serveur héritent de la configuration de segment de route (Route Segment Config) de la page ou du layout dans lequel elles sont utilisées, y compris des champs comme maxDuration.

Exemples

Gestionnaires d'événements

Bien qu'il soit courant d'utiliser des actions serveur dans des éléments <form>, elles peuvent également être invoquées avec des gestionnaires d'événements comme onClick. Par exemple, pour incrémenter un compteur de likes :

'use client'

import { incrementLike } from './actions'
import { useState } from 'react'

export default function LikeButton({ initialLikes }: { initialLikes: number }) {
  const [likes, setLikes] = useState(initialLikes)

  return (
    <>
      <p>Total Likes: {likes}</p>
      <button
        onClick={async () => {
          const updatedLikes = await incrementLike()
          setLikes(updatedLikes)
        }}
      >
        Like
      </button>
    </>
  )
}

Vous pouvez également ajouter des gestionnaires d'événements aux éléments de formulaire, par exemple pour sauvegarder un champ de formulaire onChange :

app/ui/edit-post.tsx
'use client'

import { publishPost, saveDraft } from './actions'

export default function EditPost() {
  return (
    <form action={publishPost}>
      <textarea
        name="content"
        onChange={async (e) => {
          await saveDraft(e.target.value)
        }}
      />
      <button type="submit">Publish</button>
    </form>
  )
}

Pour des cas comme celui-ci, où plusieurs événements peuvent être déclenchés rapidement, nous recommandons d'utiliser un debouncing pour éviter des invocations inutiles d'actions serveur.

useEffect

Vous pouvez utiliser le hook React useEffect pour invoquer une action serveur lorsque le composant est monté ou qu'une dépendance change. Ceci est utile pour les mutations qui dépendent d'événements globaux ou qui doivent être déclenchées automatiquement. Par exemple, onKeyDown pour les raccourcis d'application, un hook d'observateur d'intersection pour le défilement infini, ou lorsque le composant est monté pour mettre à jour un compteur de vues :

'use client'

import { incrementViews } from './actions'
import { useState, useEffect, useTransition } from 'react'

export default function ViewCount({ initialViews }: { initialViews: number }) {
  const [views, setViews] = useState(initialViews)
  const [isPending, startTransition] = useTransition()

  useEffect(() => {
    startTransition(async () => {
      const updatedViews = await incrementViews()
      setViews(updatedViews)
    })
  }, [])

  // Vous pouvez utiliser `isPending` pour donner un retour aux utilisateurs
  return <p>Total Views: {views}</p>
}

N'oubliez pas de considérer les comportements et mises en garde de useEffect.

Gestion des erreurs

Lorsqu'une erreur est levée, elle sera capturée par le fichier error.js le plus proche ou la limite <Suspense> sur le client. Voir Gestion des erreurs pour plus d'informations.

Bon à savoir :

  • En plus de lever l'erreur, vous pouvez également retourner un objet à gérer par useActionState.

Revalidation des données

Vous pouvez revalider le cache Next.js dans vos actions serveur avec l'API revalidatePath :

'use server'

import { revalidatePath } from 'next/cache'

export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }

  revalidatePath('/posts')
}

Ou invalider une récupération de données spécifique avec une étiquette de cache en utilisant revalidateTag :

'use server'

import { revalidateTag } from 'next/cache'

export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }

  revalidateTag('posts')
}

Redirection

Si vous souhaitez rediriger l'utilisateur vers une route différente après la complétion d'une action serveur, vous pouvez utiliser l'API redirect. redirect doit être appelé en dehors du bloc try/catch :

'use server'

import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'

export async function createPost(id: string) {
  try {
    // ...
  } catch (error) {
    // ...
  }

  revalidateTag('posts') // Mise à jour des posts en cache
  redirect(`/post/${id}`) // Navigation vers la nouvelle page de post
}

Cookies

Vous pouvez get, set et delete des cookies dans une action serveur en utilisant l'API cookies :

'use server'

import { cookies } from 'next/headers'

export async function exampleAction() {
  const cookieStore = await cookies()

  // Récupérer un cookie
  cookieStore.get('name')?.value

  // Définir un cookie
  cookieStore.set('name', 'Delba')

  // Supprimer un cookie
  cookieStore.delete('name')
}

Voir des exemples supplémentaires pour supprimer des cookies depuis des actions serveur.

Sécurité

Par défaut, lorsqu'une action serveur est créée et exportée, elle crée un point de terminaison HTTP public et doit être traitée avec les mêmes hypothèses de sécurité et vérifications d'autorisation. Cela signifie que même si une action serveur ou une fonction utilitaire n'est pas importée ailleurs dans votre code, elle reste accessible publiquement.

Pour améliorer la sécurité, Next.js dispose des fonctionnalités intégrées suivantes :

  • Identifiants d'action sécurisés : Next.js crée des identifiants chiffrés et non déterministes pour permettre au client de référencer et d'appeler l'action serveur. Ces identifiants sont recalculés périodiquement entre les builds pour une sécurité renforcée.
  • Élimination du code mort : Les actions serveur inutilisées (référencées par leurs identifiants) sont supprimées du bundle client pour éviter un accès public par des tiers.

Bon à savoir :

Les identifiants sont créés pendant la compilation et sont mis en cache pour un maximum de 14 jours. Ils seront régénérés lorsqu'un nouveau build est initié ou lorsque le cache de build est invalidé. Cette amélioration de sécurité réduit les risques dans les cas où une couche d'authentification est manquante. Cependant, vous devez toujours traiter les actions serveur comme des points de terminaison HTTP publics.

// app/actions.js
'use server'

// Cette action **est** utilisée dans notre application, donc Next.js
// créera un identifiant sécurisé pour permettre au client de référencer
// et d'appeler l'action serveur.
export async function updateUserAction(formData) {}

// Cette action **n'est pas** utilisée dans notre application, donc Next.js
// supprimera automatiquement ce code pendant `next build`
// et ne créera pas de point de terminaison public.
export async function deleteUserAction(formData) {}

Authentification et autorisation

Vous devez vous assurer que l'utilisateur est autorisé à effectuer l'action. Par exemple :

app/actions.ts
'use server'

import { auth } from './lib'

export function addItem() {
  const { user } = auth()
  if (!user) {
    throw new Error('You must be signed in to perform this action')
  }

  // ...
}

Fermetures et chiffrement

Définir une action serveur dans un composant crée une fermeture (closure) où l'action a accès à la portée de la fonction externe. Par exemple, l'action publish a accès à la variable publishVersion :

export default async function Page() {
  const publishVersion = await getLatestVersion();

  async function publish() {
    "use server";
    if (publishVersion !== await getLatestVersion()) {
      throw new Error('The version has changed since pressing publish');
    }
    ...
  }

  return (
    <form>
      <button formAction={publish}>Publish</button>
    </form>
  );
}

Les fermetures sont utiles lorsque vous avez besoin de capturer un instantané de données (par exemple publishVersion) au moment du rendu pour pouvoir l'utiliser plus tard lorsque l'action est invoquée.

Cependant, pour que cela se produise, les variables capturées sont envoyées au client et retournées au serveur lorsque l'action est invoquée. Pour empêcher l'exposition de données sensibles au client, Next.js chiffre automatiquement les variables capturées. Une nouvelle clé privée est générée pour chaque action à chaque fois qu'une application Next.js est buildée. Cela signifie que les actions ne peuvent être invoquées que pour un build spécifique.

Bon à savoir : Nous ne recommandons pas de compter uniquement sur le chiffrement pour empêcher l'exposition de valeurs sensibles au client. À la place, vous devriez utiliser les API React taint pour empêcher proactivement l'envoi de données spécifiques au client.

Remplacement des clés de chiffrement (usage avancé)

Lorsque vous auto-hébergez votre application Next.js sur plusieurs serveurs, chaque instance de serveur peut se retrouver avec une clé de chiffrement différente, ce qui peut entraîner des incohérences.

Pour atténuer ce problème, vous pouvez remplacer la clé de chiffrement en utilisant la variable d'environnement process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY. Spécifier cette variable garantit que vos clés de chiffrement persistent entre les builds et que toutes les instances de serveur utilisent la même clé. Cette variable doit être chiffrée avec AES-GCM.

Il s'agit d'un cas d'usage avancé où un comportement de chiffrement cohérent entre plusieurs déploiements est critique pour votre application. Vous devriez envisager des pratiques de sécurité standard telles que la rotation des clés et la signature.

Bon à savoir : Les applications Next.js déployées sur Vercel gèrent cela automatiquement.

Origines autorisées (usage avancé)

Comme les Actions Serveur peuvent être invoquées dans un élément <form>, cela les expose aux attaques CSRF.

En arrière-plan, les Actions Serveur utilisent la méthode POST, et seule cette méthode HTTP est autorisée pour les invoquer. Cela empêche la plupart des vulnérabilités CSRF dans les navigateurs modernes, en particulier avec les cookies SameSite étant activés par défaut.

Comme protection supplémentaire, les Actions Serveur dans Next.js comparent également l'en-tête Origin à l'en-tête Host (ou X-Forwarded-Host). Si ceux-ci ne correspondent pas, la requête sera abandonnée. En d'autres termes, les Actions Serveur ne peuvent être invoquées que sur le même hôte que la page qui les héberge.

Pour les grandes applications utilisant des proxies inversés ou des architectures backend à plusieurs niveaux (où l'API serveur diffère du domaine de production), il est recommandé d'utiliser l'option de configuration serverActions.allowedOrigins pour spécifier une liste d'origines sûres. L'option accepte un tableau de chaînes de caractères.

next.config.js
/** @type {import('next').NextConfig} */
module.exports = {
  experimental: {
    serverActions: {
      allowedOrigins: ['my-proxy.com', '*.my-proxy.com'],
    },
  },
}

En savoir plus sur la Sécurité et les Actions Serveur.

Ressources supplémentaires

Pour plus d'informations, consultez la documentation React suivante :