Actions serveur et mutations
Les actions serveur sont des fonctions asynchrones exécutées sur le serveur. Elles peuvent être utilisé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 formulaires et 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 :
// Composant serveur
export default function Page() {
// Action serveur
async function create() {
'use server'
// ...
}
return (
// ...
)
}
// Composant serveur
export default function Page() {
// Action serveur
async function create() {
'use server'
// ...
}
return (
// ...
)
}
Composants client
Les composants client ne peuvent importer que des actions utilisant la directive "use server"
au niveau du module.
Pour appeler une action serveur dans un composant client, créez un nouveau fichier et ajoutez la directive "use server"
en haut. Toutes les fonctions dans le fichier seront marquées comme actions serveur pouvant être réutilisées dans les composants client et serveur :
'use server'
export async function create() {
// ...
}
'use server'
export async function create() {
// ...
}
Vous pouvez également passer une action serveur à un composant client comme prop :
<ClientComponent updateItem={updateItem} />
'use client'
export default function ClientComponent({ updateItem }) {
return <form action={updateItem}>{/* ... */}</form>
}
Comportement
- Les actions serveur peuvent être invoquées en utilisant l'attribut
action
dans un élément<form>
:- Les composants serveur supportent 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 à
<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 avec l'architecture de cache et revalidation de Next.js. Lorsqu'une action est invoquée, Next.js peut retourner à la fois l'UI 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 où elles sont utilisées.
- Les actions serveur héritent de la configuration du segment de route de la page ou du layout où elles sont utilisées, y compris des champs comme
maxDuration
.
Exemples
Formulaires
React étend l'élément HTML <form>
pour permettre l'invocation d'actions serveur avec la prop action
.
Lorsqu'elle est invoquée dans un formulaire, l'action reçoit automatiquement l'objet FormData
. Vous n'avez pas besoin d'utiliser React useState
pour gérer les champs, vous pouvez extraire les données en utilisant les méthodes natives de FormData
:
export default function Page() {
async function createInvoice(formData: FormData) {
'use server'
const rawFormData = {
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
}
// muter les données
// revalider le cache
}
return <form action={createInvoice}>...</form>
}
export default function Page() {
async function createInvoice(formData) {
'use server'
const rawFormData = {
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
}
// muter les données
// revalider le cache
}
return <form action={createInvoice}>...</form>
}
Bon à savoir :
- Exemple : Formulaire avec états de chargement et d'erreur
- Lorsque vous travaillez avec des formulaires ayant de nombreux champs, vous pouvez envisager d'utiliser la méthode
entries()
avecObject.fromEntries()
de JavaScript. Par exemple :const rawFormData = Object.fromEntries(formData)
. Une chose à noter est que leformData
inclura des propriétés supplémentaires$ACTION_
.- Voir la documentation de React sur
<form>
pour en savoir plus.
Passage d'arguments supplémentaires
Vous pouvez passer des arguments supplémentaires à une action serveur en utilisant la méthode JavaScript bind
.
'use client'
import { updateUser } from './actions'
export function UserProfile({ userId }: { userId: string }) {
const updateUserWithId = updateUser.bind(null, userId)
return (
<form action={updateUserWithId}>
<input type="text" name="name" />
<button type="submit">Mettre à jour le nom</button>
</form>
)
}
'use client'
import { updateUser } from './actions'
export function UserProfile({ userId }) {
const updateUserWithId = updateUser.bind(null, userId)
return (
<form action={updateUserWithId}>
<input type="text" name="name" />
<button type="submit">Mettre à jour le nom</button>
</form>
)
}
L'action serveur recevra l'argument userId
, en plus des données du formulaire :
'use server'
export async function updateUser(userId, formData) {
// ...
}
Bon à savoir :
- Une alternative est de passer les arguments comme champs cachés dans le formulaire (par exemple
<input type="hidden" name="userId" value={userId} />
). Cependant, la valeur fera partie du HTML rendu et ne sera pas encodée..bind
fonctionne dans les composants serveur et client. Il supporte également l'amélioration progressive.
États en attente
Vous pouvez utiliser le hook React useFormStatus
pour afficher un état en attente pendant la soumission du formulaire.
useFormStatus
retourne le statut pour un<form>
spécifique, donc il doit être défini comme un enfant de l'élément<form>
.useFormStatus
est un hook React et doit donc être utilisé dans un composant client.
<SubmitButton />
peut ensuite être imbriqué dans n'importe quel formulaire :
import { SubmitButton } from '@/app/submit-button'
import { createItem } from '@/app/actions'
// Composant serveur
export default async function Home() {
return (
<form action={createItem}>
<input type="text" name="field-name" />
<SubmitButton />
</form>
)
}
import { SubmitButton } from '@/app/submit-button'
import { createItem } from '@/app/actions'
// Composant serveur
export default async function Home() {
return (
<form action={createItem}>
<input type="text" name="field-name" />
<SubmitButton />
</form>
)
}
Validation côté serveur et gestion des erreurs
Nous recommandons d'utiliser la validation HTML comme required
et type="email"
pour une validation de base côté client.
Pour une validation plus avancée côté serveur, vous pouvez utiliser une bibliothèque comme zod pour valider les champs du formulaire avant de muter les données :
'use server'
import { z } from 'zod'
const schema = z.object({
email: z.string({
invalid_type_error: 'Email invalide',
}),
})
export default async function createUser(formData: FormData) {
const validatedFields = schema.safeParse({
email: formData.get('email'),
})
// Retourner tôt si les données du formulaire sont invalides
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// Muter les données
}
'use server'
import { z } from 'zod'
const schema = z.object({
email: z.string({
invalid_type_error: 'Email invalide',
}),
})
export default async function createsUser(formData) {
const validatedFields = schema.safeParse({
email: formData.get('email'),
})
// Retourner tôt si les données du formulaire sont invalides
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// Muter les données
}
Une fois les champs validés côté serveur, vous pouvez retourner un objet sérialisable dans votre action et utiliser le hook React useFormState
pour afficher un message à l'utilisateur.
- En passant l'action à
useFormState
, la signature de la fonction de l'action change pour recevoir un nouveau paramètreprevState
ouinitialState
comme premier argument. useFormState
est un hook React et doit donc être utilisé dans un composant client.
'use server'
export async function createUser(prevState: any, formData: FormData) {
// ...
return {
message: 'Veuillez entrer un email valide',
}
}
'use server'
export async function createUser(prevState, formData) {
// ...
return {
message: 'Veuillez entrer un email valide',
}
}
Ensuite, vous pouvez passer votre action au hook useFormState
et utiliser le state
retourné pour afficher un message d'erreur.
'use client'
import { useFormState } from 'react-dom'
import { createUser } from '@/app/actions'
const initialState = {
message: '',
}
export function Signup() {
const [state, formAction] = useFormState(createUser, initialState)
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite" className="sr-only">
{state?.message}
</p>
<button>S'inscrire</button>
</form>
)
}
'use client'
import { useFormState } from 'react-dom'
import { createUser } from '@/app/actions'
const initialState = {
message: '',
}
export function Signup() {
const [state, formAction] = useFormState(createUser, initialState)
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite" className="sr-only">
{state?.message}
</p>
<button>S'inscrire</button>
</form>
)
}
Bon à savoir :
- Avant de muter les données, vous devez toujours vous assurer qu'un utilisateur est également autorisé à effectuer l'action. Voir Authentification et autorisation.
Mises à jour optimistes
Vous pouvez utiliser le hook React useOptimistic
pour mettre à jour de manière optimiste l'UI avant que l'action serveur ne se termine, plutôt que d'attendre la réponse :
'use client'
import { useOptimistic } from 'react'
import { send } from './actions'
type Message = {
message: string
}
export function Thread({ messages }: { messages: Message[] }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic<
Message[],
string
>(messages, (state, newMessage) => [...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 { 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>
)
}
Éléments imbriqués
Vous pouvez invoquer une action serveur dans des éléments imbriqués à l'intérieur de <form>
comme <button>
, <input type="submit">
, et <input type="image">
. Ces éléments acceptent la prop formAction
ou des gestionnaires d'événements.
C'est utile dans les cas où vous souhaitez appeler plusieurs actions serveur dans un formulaire. Par exemple, vous pouvez créer un élément <button>
spécifique pour sauvegarder un brouillon de poste en plus de le publier. Voir la documentation de React sur <form>
pour plus d'informations.
Soumission programmatique de formulaire
Vous pouvez déclencher la soumission d'un formulaire en utilisant la méthode requestSubmit()
. Par exemple, lorsque l'utilisateur appuie sur ⌘
+ Entrée
, vous pouvez écouter l'événement onKeyDown
:
'use client'
export function Entry() {
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (
(e.ctrlKey || e.metaKey) &&
(e.key === 'Enter' || e.key === 'NumpadEnter')
) {
e.preventDefault()
e.currentTarget.form?.requestSubmit()
}
}
return (
<div>
<textarea name="entry" rows={20} required onKeyDown={handleKeyDown} />
</div>
)
}
'use client'
export function Entry() {
const handleKeyDown = (e) => {
if (
(e.ctrlKey || e.metaKey) &&
(e.key === 'Enter' || e.key === 'NumpadEnter')
) {
e.preventDefault()
e.currentTarget.form?.requestSubmit()
}
}
return (
<div>
<textarea name="entry" rows={20} required onKeyDown={handleKeyDown} />
</div>
)
}
Cela déclenchera la soumission du <form>
ancêtre le plus proche, ce qui invoquera l'Action Serveur.
Éléments non-formulaire
Bien qu'il soit courant d'utiliser les Actions Serveur dans des éléments <form>
, elles peuvent également être invoquées depuis d'autres parties de votre code, comme les gestionnaires d'événements et useEffect
.
Gestionnaires d'événements
Vous pouvez invoquer une Action Serveur depuis des gestionnaires d'événements comme onClick
. Par exemple, pour incrémenter un compteur de likes :
Pour améliorer l'expérience utilisateur, nous recommandons d'utiliser d'autres API React comme useOptimistic
et useTransition
pour mettre à jour l'interface avant que l'Action Serveur ne termine son exécution sur le serveur, ou pour afficher un état en attente.
Vous pouvez également ajouter des gestionnaires d'événements aux éléments de formulaire, par exemple pour sauvegarder un champ de formulaire onChange
:
'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">Publier</button>
</form>
)
}
Pour des cas comme celui-ci, où plusieurs événements peuvent être déclenchés rapidement, nous recommandons d'utiliser le 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'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 } from 'react'
export default function ViewCount({ initialViews }: { initialViews: number }) {
const [views, setViews] = useState(initialViews)
useEffect(() => {
const updateViews = async () => {
const updatedViews = await incrementViews()
setViews(updatedViews)
}
updateViews()
}, [])
return <p>Total Views : {views}</p>
}
'use client'
import { incrementViews } from './actions'
import { useState, useEffect } from 'react'
export default function ViewCount({ initialViews }: { initialViews: number }) {
const [views, setViews] = useState(initialViews)
useEffect(() => {
const updateViews = async () => {
const updatedViews = await incrementViews()
setViews(updatedViews)
}
updateViews()
}, [])
return <p>Total Views : {views}</p>
}
N'oubliez pas de prendre en compte le comportement et les 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. Nous recommandons d'utiliser try/catch
pour retourner les erreurs à gérer par votre interface.
Par exemple, votre Action Serveur pourrait gérer les erreurs lors de la création d'un nouvel élément en retournant un message :
'use server'
export async function createTodo(prevState: any, formData: FormData) {
try {
// Mutate data
} catch (e) {
throw new Error('Échec de la création de la tâche')
}
}
'use server'
export async function createTodo(prevState, formData) {
try {
// Mutate data
} catch (e) {
throw new Error('Échec de la création de la tâche')
}
}
Bon à savoir :
- En plus de lever l'erreur, vous pouvez également retourner un objet à gérer par
useFormState
. Voir Validation côté serveur et gestion des erreurs.
Révalidation des données
Vous pouvez révalider 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')
}
'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')
}
'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 l'exécution 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') // Mettre à jour les posts en cache
redirect(`/post/${id}`) // Naviguer vers la nouvelle page de post
}
'use server'
import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'
export async function createPost(id) {
try {
// ...
} catch (error) {
// ...
}
revalidateTag('posts') // Mettre à jour les posts en cache
redirect(`/post/${id}`) // Naviguer 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() {
// Get cookie
const value = cookies().get('name')?.value
// Set cookie
cookies().set('name', 'Delba')
// Delete cookie
cookies().delete('name')
}
'use server'
import { cookies } from 'next/headers'
export async function exampleAction() {
// Get cookie
const value = cookies().get('name')?.value
// Set cookie
cookies().set('name', 'Delba')
// Delete cookie
cookies().delete('name')
}
Voir des exemples supplémentaires pour supprimer des cookies depuis des Actions Serveur.
Sécurité
Authentification et autorisation
Vous devriez traiter les Actions Serveur comme vous le feriez pour des points de terminaison d'API publics, et vous assurer que l'utilisateur est autorisé à effectuer l'action. Par exemple :
'use server'
import { auth } from './lib'
export function addItem() {
const { user } = auth()
if (!user) {
throw new Error('Vous devez être connecté pour effectuer cette 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 function Page() {
const publishVersion = await getLatestVersion();
async function publish(formData: FormData) {
"use server";
if (publishVersion !== await getLatestVersion()) {
throw new Error('La version a changé depuis l\'appui sur publier');
}
...
}
return <button action={publish}>Publier</button>;
}
export default function Page() {
const publishVersion = await getLatestVersion();
async function publish() {
"use server";
if (publishVersion !== await getLatestVersion()) {
throw new Error('La version a changé depuis l\'appui sur publier');
}
...
}
return <button action={publish}>Publier</button>;
}
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 les données sensibles d'être exposées 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 construite. Cela signifie que les actions ne peuvent être invoquées que pour une construction spécifique.
Bon à savoir : Nous ne recommandons pas de compter uniquement sur le chiffrement pour empêcher les valeurs sensibles d'être exposées au client. À la place, vous devriez utiliser les API React taint pour empêcher de manière proactive certaines données d'être envoyées au client.
Remplacement des clés de chiffrement (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 cela, 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 sont persistantes entre les constructions, et que toutes les instances de serveur utilisent la même clé.
Ceci est un cas d'utilisation avancé où un comportement de chiffrement cohérent entre plusieurs déploiements est critique pour votre application. Vous devriez considérer les pratiques de sécurité standard comme 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 (avancé)
Puisque 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 la valeur 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 qui utilisent des proxies inversés ou des architectures backend à plusieurs couches (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.
/** @type {import('next').NextConfig} */
module.exports = {
experimental: {
serverActions: {
allowedOrigins: ['my-proxy.com', '*.my-proxy.com'],
},
},
}
En savoir plus sur Sécurité et Actions Serveur.
Ressources supplémentaires
Pour plus d'informations sur les Actions Serveur, consultez les documents React suivants :