Formulaires et Mutations

Les formulaires vous permettent de créer et mettre à jour des données dans les applications web. Next.js offre une solution puissante pour gérer les soumissions de formulaires et les mutations de données en utilisant les Actions Serveur.

Exemples

Fonctionnement des Actions Serveur

Avec les Actions Serveur, vous n'avez pas besoin de créer manuellement des points d'API. À la place, vous définissez des fonctions serveur asynchrones qui peuvent être appelées directement depuis vos composants.

🎥 Regarder : En savoir plus sur les formulaires et mutations avec le routeur App → YouTube (10 minutes).

Les Actions Serveur peuvent être définies dans des Composants Serveur ou appelées depuis des Composants Client. Définir l'action dans un Composant Serveur permet au formulaire de fonctionner sans JavaScript, offrant une amélioration progressive.

Activez les Actions Serveur dans votre fichier next.config.js :

next.config.js
module.exports = {
  experimental: {
    serverActions: true,
  },
}

Bon à savoir :

  • Les formulaires appelant des Actions Serveur depuis des Composants Serveur peuvent fonctionner sans JavaScript.
  • Les formulaires appelant des Actions Serveur depuis des Composants Client mettront les soumissions en file d'attente si JavaScript n'est pas encore chargé, en priorisant l'hydratation client.
  • Les Actions Serveur héritent du runtime de la page ou du layout dans lequel elles sont utilisées.
  • Les Actions Serveur fonctionnent avec des routes entièrement statiques (y compris la revalidation de données avec ISR).

Revalidation des Données en Cache

Les Actions Serveur s'intègrent profondément avec l'architecture de cache et revalidation de Next.js. Lorsqu'un formulaire est soumis, l'Action Serveur peut mettre à jour les données en cache et revalider toutes les clés de cache qui doivent changer.

Plutôt que d'être limité à un seul formulaire par route comme dans les applications traditionnelles, les Actions Serveur permettent d'avoir plusieurs actions par route. De plus, le navigateur n'a pas besoin de s'actualiser lors de la soumission d'un formulaire. En un seul aller-retour réseau, Next.js peut retourner à la fois l'UI mise à jour et les données rafraîchies.

Consultez les exemples ci-dessous pour revalider des données depuis des Actions Serveur.

Exemples

Formulaires Serveur Uniquement

Pour créer un formulaire serveur uniquement, définissez l'Action Serveur dans un Composant Serveur. L'action peut être définie en ligne avec la directive "use server" en haut de la fonction, ou dans un fichier séparé avec la directive en haut du fichier.

export default function Page() {
  async function create(formData: FormData) {
    'use server'

    // muter les données
    // revalider le cache
  }

  return <form action={create}>...</form>
}
export default function Page() {
  async function create(formData) {
    'use server'

    // muter les données
    // revalider le cache
  }

  return <form action={create}>...</form>
}

Bon à savoir : <form action={create}> prend le type de données FormData. Dans l'exemple ci-dessus, le FormData soumis via le form HTML est accessible dans l'action serveur create.

Revalidation des Données

Les Actions Serveur vous permettent d'invalider le Cache Next.js à la demande. Vous pouvez invalider un segment de route entier avec revalidatePath :

'use server'

import { revalidatePath } from 'next/cache'

export default async function submit() {
  await submitForm()
  revalidatePath('/')
}
'use server'

import { revalidatePath } from 'next/cache'

export default async function submit() {
  await submitForm()
  revalidatePath('/')
}

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 default async function submit() {
  await addPost()
  revalidateTag('posts')
}
'use server'

import { revalidateTag } from 'next/cache'

export default async function submit() {
  await addPost()
  revalidateTag('posts')
}

Redirection

Si vous souhaitez rediriger l'utilisateur vers une route différente après l'exécution d'une Action Serveur, vous pouvez utiliser redirect et n'importe quelle URL absolue ou relative :

'use server'

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

export default async function submit() {
  const id = await addPost()
  revalidateTag('posts') // Mettre à jour les posts en cache
  redirect(`/post/${id}`) // Naviguer vers la nouvelle route
}
'use server'

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

export default async function submit() {
  const id = await addPost()
  revalidateTag('posts') // Mettre à jour les posts en cache
  redirect(`/post/${id}`) // Naviguer vers la nouvelle route
}

Validation de Formulaire

Nous recommandons d'utiliser la validation HTML comme required et type="email" pour une validation de formulaire basique.

Pour une validation côté serveur plus avancée, utilisez une bibliothèque de validation de schéma comme zod pour valider la structure des données de formulaire analysées :

import { z } from 'zod'

const schema = z.object({
  // ...
})

export default async function submit(formData: FormData) {
  const parsed = schema.parse({
    id: formData.get('id'),
  })
  // ...
}
import { z } from 'zod'

const schema = z.object({
  // ...
})

export default async function submit(formData) {
  const parsed = schema.parse({
    id: formData.get('id'),
  })
  // ...
}

Affichage de l'État de Chargement

Utilisez le hook useFormStatus pour afficher un état de chargement lorsqu'un formulaire est en cours de soumission sur le serveur. Le hook useFormStatus ne peut être utilisé que comme enfant d'un élément form utilisant une Action Serveur.

Par exemple, le bouton de soumission suivant :

'use client'

import { experimental_useFormStatus as useFormStatus } from 'react-dom'

export function SubmitButton() {
  const { pending } = useFormStatus()

  return (
    <button type="submit" aria-disabled={pending}>
      Ajouter
    </button>
  )
}
'use client'

import { experimental_useFormStatus as useFormStatus } from 'react-dom'

export function SubmitButton() {
  const { pending } = useFormStatus()

  return (
    <button type="submit" aria-disabled={pending}>
      Ajouter
    </button>
  )
}

<SubmitButton /> peut ensuite être utilisé dans un formulaire avec une Action Serveur :

import { SubmitButton } from '@/app/submit-button'

export default async function Home() {
  return (
    <form action={...}>
      <input type="text" name="field-name" />
      <SubmitButton />
    </form>
  )
}
import { SubmitButton } from '@/app/submit-button'

export default async function Home() {
  return (
    <form action={...}>
      <input type="text" name="field-name" />
      <SubmitButton />
    </form>
  )
}

Gestion des erreurs

Les Actions Serveur peuvent également retourner des objets sérialisables. Par exemple, votre Action Serveur peut gérer les erreurs lors de la création d'un nouvel élément :

'use server'

export async function createTodo(prevState: any, formData: FormData) {
  try {
    await createItem(formData.get('todo'))
    return revalidatePath('/')
  } catch (e) {
    return { message: 'Failed to create' }
  }
}
'use server'

export async function createTodo(prevState, formData) {
  try {
    await createItem(formData.get('todo'))
    return revalidatePath('/')
  } catch (e) {
    return { message: 'Failed to create' }
  }
}

Ensuite, depuis un Composant Client, vous pouvez lire cette valeur et afficher un message d'erreur.

'use client'

import { experimental_useFormState as useFormState } from 'react-dom'
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
import { createTodo } from '@/app/actions'

const initialState = {
  message: null,
}

function SubmitButton() {
  const { pending } = useFormStatus()

  return (
    <button type="submit" aria-disabled={pending}>
      Ajouter
    </button>
  )
}

export function AddForm() {
  const [state, formAction] = useFormState(createTodo, initialState)

  return (
    <form action={formAction}>
      <label htmlFor="todo">Saisir une tâche</label>
      <input type="text" id="todo" name="todo" required />
      <SubmitButton />
      <p aria-live="polite" className="sr-only">
        {state?.message}
      </p>
    </form>
  )
}
'use client'

import { experimental_useFormState as useFormState } from 'react-dom'
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
import { createTodo } from '@/app/actions'

const initialState = {
  message: null,
}

function SubmitButton() {
  const { pending } = useFormStatus()

  return (
    <button type="submit" aria-disabled={pending}>
      Ajouter
    </button>
  )
}

export function AddForm() {
  const [state, formAction] = useFormState(createTodo, initialState)

  return (
    <form action={formAction}>
      <label htmlFor="todo">Saisir une tâche</label>
      <input type="text" id="todo" name="todo" required />
      <SubmitButton />
      <p aria-live="polite" className="sr-only">
        {state?.message}
      </p>
    </form>
  )
}

Mises à jour optimistes

Utilisez useOptimistic pour mettre à jour l'interface utilisateur de manière optimiste avant que l'Action Serveur ne se termine, plutôt que d'attendre la réponse :

'use client'

import { experimental_useOptimistic as useOptimistic } from 'react'
import { send } from './actions'

type Message = {
  message: string
}

export function Thread({ messages }: { messages: Message[] }) {
  const [optimisticMessages, addOptimisticMessage] = useOptimistic<Message[]>(
    messages,
    (state: Message[], newMessage: string) => [
      ...state,
      { message: newMessage },
    ]
  )

  return (
    <div>
      {optimisticMessages.map((m, k) => (
        <div key={k}>{m.message}</div>
      ))}
      <form
        action={async (formData: FormData) => {
          const message = formData.get('message')
          addOptimisticMessage(message)
          await send(message)
        }}
      >
        <input type="text" name="message" />
        <button type="submit">Envoyer</button>
      </form>
    </div>
  )
}
'use client'

import { experimental_useOptimistic as useOptimistic } from 'react'
import { send } from './actions'

export function Thread({ messages }) {
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) => [...state, { message: newMessage }]
  )

  return (
    <div>
      {optimisticMessages.map((m) => (
        <div>{m.message}</div>
      ))}
      <form
        action={async (formData) => {
          const message = formData.get('message')
          addOptimisticMessage(message)
          await send(message)
        }}
      >
        <input type="text" name="message" />
        <button type="submit">Envoyer</button>
      </form>
    </div>
  )
}

Définition des cookies

Vous pouvez définir des cookies dans une Action Serveur en utilisant la fonction cookies :

'use server'

import { cookies } from 'next/headers'

export async function create() {
  const cart = await createCart()
  cookies().set('cartId', cart.id)
}
'use server'

import { cookies } from 'next/headers'

export async function create() {
  const cart = await createCart()
  cookies().set('cartId', cart.id)
}

Lecture des cookies

Vous pouvez lire les cookies dans une Action Serveur en utilisant la fonction cookies :

'use server'

import { cookies } from 'next/headers'

export async function read() {
  const auth = cookies().get('authorization')?.value
  // ...
}
'use server'

import { cookies } from 'next/headers'

export async function read() {
  const auth = cookies().get('authorization')?.value
  // ...
}

Suppression des cookies

Vous pouvez supprimer des cookies dans une Action Serveur en utilisant la fonction cookies :

'use server'

import { cookies } from 'next/headers'

export async function delete() {
  cookies().delete('name')
  // ...
}
'use server'

import { cookies } from 'next/headers'

export async function delete() {
  cookies().delete('name')
  // ...
}

Voir des exemples supplémentaires pour supprimer des cookies depuis des Actions Serveur.