Comment créer des formulaires avec les Server Actions
Les Server Actions de React sont des fonctions serveur qui s'exécutent côté serveur. Elles peuvent être appelées dans les composants Serveur et Client pour gérer les soumissions de formulaires. Ce guide vous montrera comment créer des formulaires dans Next.js avec les Server Actions.
Fonctionnement
React étend l'élément HTML <form>
pour permettre l'invocation des Server Actions via l'attribut action
.
Lorsqu'elle est utilisée dans un formulaire, la fonction reçoit automatiquement l'objet FormData
. Vous pouvez ensuite 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 : Pour les formulaires avec plusieurs champs, vous pouvez utiliser la méthode
entries()
avecObject.fromEntries()
de JavaScript. Par exemple :const rawFormData = Object.fromEntries(formData)
.
Passage d'arguments supplémentaires
En dehors des champs de formulaire, vous pouvez passer des arguments supplémentaires à une fonction serveur en utilisant la méthode JavaScript bind
. Par exemple, pour passer l'argument userId
à la fonction serveur updateUser
:
'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>
)
}
La fonction serveur recevra userId
comme argument supplémentaire :
'use server'
export async function updateUser(userId: string, formData: FormData) {}
'use server'
export async function updateUser(userId, formData) {}
Bon à savoir :
- Une alternative consiste à passer les arguments sous forme de 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 et prend en charge l'amélioration progressive.
Validation des formulaires
Les formulaires peuvent être validés côté client ou serveur.
- Pour la validation côté client, vous pouvez utiliser les attributs HTML comme
required
ettype="email"
pour une validation basique. - Pour la validation côté serveur, vous pouvez utiliser une bibliothèque comme zod pour valider les champs du formulaire. Par exemple :
'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'),
})
// Retour anticipé 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'),
})
// Retour anticipé si les données du formulaire sont invalides
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// Muter les données
}
Erreurs de validation
Pour afficher les erreurs ou messages de validation, transformez le composant qui définit le <form>
en composant Client et utilisez useActionState
de React.
Avec useActionState
, la signature de la fonction serveur changera pour recevoir un nouveau paramètre prevState
ou initialState
comme premier argument.
'use server'
import { z } from 'zod'
export async function createUser(initialState: any, formData: FormData) {
const validatedFields = schema.safeParse({
email: formData.get('email'),
})
// ...
}
'use server'
import { z } from 'zod'
// ...
export async function createUser(initialState, formData) {
const validatedFields = schema.safeParse({
email: formData.get('email'),
})
// ...
}
Vous pouvez ensuite afficher conditionnellement le message d'erreur en fonction de l'objet state
.
'use client'
import { useActionState } from 'react'
import { createUser } from '@/app/actions'
const initialState = {
message: '',
}
export function Signup() {
const [state, formAction, pending] = useActionState(createUser, initialState)
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite">{state?.message}</p>
<button disabled={pending}>S'inscrire</button>
</form>
)
}
'use client'
import { useActionState } from 'react'
import { createUser } from '@/app/actions'
const initialState = {
message: '',
}
export function Signup() {
const [state, formAction, pending] = useActionState(createUser, initialState)
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite">{state?.message}</p>
<button disabled={pending}>S'inscrire</button>
</form>
)
}
États d'attente
Le hook useActionState
expose un booléen pending
qui peut être utilisé pour afficher un indicateur de chargement ou désactiver le bouton de soumission pendant l'exécution de l'action.
'use client'
import { useActionState } from 'react'
import { createUser } from '@/app/actions'
export function Signup() {
const [state, formAction, pending] = useActionState(createUser, initialState)
return (
<form action={formAction}>
{/* Autres éléments du formulaire */}
<button disabled={pending}>S'inscrire</button>
</form>
)
}
'use client'
import { useActionState } from 'react'
import { createUser } from '@/app/actions'
export function Signup() {
const [state, formAction, pending] = useActionState(createUser, initialState)
return (
<form action={formAction}>
{/* Autres éléments du formulaire */}
<button disabled={pending}>S'inscrire</button>
</form>
)
}
Alternativement, vous pouvez utiliser le hook useFormStatus
pour afficher un indicateur de chargement pendant l'exécution de l'action. Avec ce hook, vous devrez créer un composant séparé pour afficher l'indicateur. Par exemple, pour désactiver le bouton pendant l'attente :
Vous pouvez ensuite imbriquer le composant SubmitButton
dans le formulaire :
import { SubmitButton } from './button'
import { createUser } from '@/app/actions'
export function Signup() {
return (
<form action={createUser}>
{/* Autres éléments du formulaire */}
<SubmitButton />
</form>
)
}
import { SubmitButton } from './button'
import { createUser } from '@/app/actions'
export function Signup() {
return (
<form action={createUser}>
{/* Autres éléments du formulaire */}
<SubmitButton />
</form>
)
}
Bon à savoir : Dans React 19,
useFormStatus
inclut des clés supplémentaires sur l'objet retourné, comme data, method et action. Si vous n'utilisez pas React 19, seule la clépending
est disponible.
Mises à jour optimistes
Vous pouvez utiliser le hook React useOptimistic
pour mettre à jour l'interface de manière optimiste avant que la fonction serveur ne termine son exécution, 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 }])
const formAction = async (formData: FormData) => {
const message = formData.get('message') as string
addOptimisticMessage(message)
await send(message)
}
return (
<div>
{optimisticMessages.map((m, i) => (
<div key={i}>{m.message}</div>
))}
<form action={formAction}>
<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 }]
)
const formAction = async (formData) => {
const message = formData.get('message')
addOptimisticMessage(message)
await send(message)
}
return (
<div>
{optimisticMessages.map((m) => (
<div>{m.message}</div>
))}
<form action={formAction}>
<input type="text" name="message" />
<button type="submit">Envoyer</button>
</form>
</div>
)
}
Éléments de formulaire imbriqués
Vous pouvez appeler des Server Actions 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 lorsque vous souhaitez appeler plusieurs Server Actions dans un formulaire. Par exemple, vous pouvez créer un élément <button>
spécifique pour enregistrer un brouillon de publication en plus de le publier. Voir la documentation React sur <form>
pour plus d'informations.
Soumission programmatique de formulaire
Vous pouvez déclencher une soumission de formulaire programmatiquement en utilisant la méthode requestSubmit()
. Par exemple, lorsque l'utilisateur soumet un formulaire avec le raccourci clavier ⌘
+ 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 la fonction serveur.