Crée des formulaires modernes, gère la validation et l’état avec Zod et useActionState.
Renseigne ton email pour débloquer immédiatement cette formation gratuite.
Dans ce chapitre du module Persistance et authentification avec Prisma, on va créer et sécuriser deux formulaires clés : l’inscription et la connexion. Tu y découvriras :
use server
) pour traiter les formulaires côté serveur.Next.js 15 introduit les Server Components et les Server Actions. Ici, les pages app/login
et app/register
sont des RSC (React Server Components). Elles vont importer un composant <LoginForm />
ou <RegisterForm />
qui, lui, est un Client Component et utilise useActionState
pour piloter la soumission du formulaire.
Server Components (avec export default async function
) ne supportent pas les hooks React.
Les formulaires et leurs interactions dynamiques se font donc dans un Client Component.
On commence par créer la route /login
. Cette page est un RSC :
elle vérifie si l’utilisateur est déjà connecté et le redirige si besoin.
1'use server';23import { redirect } from 'next/navigation';4import { getOptionalUser } from '../server/session';5import { LoginForm } from '../LoginForm';67export default async function Login() {8const user = await getOptionalUser();9if (user) {10redirect('/');11}12return <LoginForm />;13}
Le formulaire de connexion est un Client Component :
il utilise useActionState
pour appeler l’action serveur login
et afficher les erreurs.
1'use client';2import { useActionState, useEffect, useRef } from 'react';3import { login } from './server/auth';45export function LoginForm() {6const [state, formAction, pending] = useActionState(login, {7errors: { email: undefined, password: undefined },8email: '',9password: '',10});1112const emailRef = useRef<HTMLInputElement>(null);13const passwordRef = useRef<HTMLInputElement>(null);1415useEffect(() => {16if (state?.errors?.email) {17emailRef.current?.focus();18} else if (state?.errors?.password) {19passwordRef.current?.focus();20}21}, [state?.errors]);2223return (24<form action={formAction} className="space-y-4">25<div>26<label htmlFor="email">Email address</label>27<input28id="email"29name="email"30type="email"31ref={emailRef}32defaultValue={state.email}33required34className={`border ${35state.errors?.email ? 'border-red-500' : 'border-gray-300'36}`}37/>38{state.errors?.email && (39<p className="text-red-600">{state.errors.email.join(', ')}</p>40)}41</div>4243<div>44<label htmlFor="password">Password</label>45<input46id="password"47name="password"48type="password"49ref={passwordRef}50defaultValue={state.password}51required52className={`border ${53state.errors?.password ? 'border-red-500' : 'border-gray-300'54}`}55/>56{state.errors?.password && (57<p className="text-red-600">{state.errors.password.join(', ')}</p>58)}59</div>6061<button62type="submit"63disabled={pending}64className="bg-blue-600 text-white px-4 py-2 rounded"65>66{pending ? 'Signing in...' : 'Sign in'}67</button>68</form>69);70}
🔄 useActionState
permet de piloter la soumission, récupérer les erreurs
validées par Zod et afficher un loader automatiquement.
Dans app/server/auth.ts
, on définit la fonction login
marquée avec "use server"
. Elle :
setUserId
.1'use server';2import { prisma } from './db';3import { compare } from 'bcryptjs';4import { redirect } from 'next/navigation';5import { z } from 'zod';6import { setUserId } from './session';78const LoginSchema = z.object({9email: z.string().email(),10password: z.string().min(6),11});1213export async function login(prevState: unknown, formData: FormData) {14const email = formData.get('email') as string;15const password = formData.get('password') as string;1617const result = LoginSchema.safeParse({ email, password });18if (!result.success) {19return { errors: result.error.flatten().fieldErrors, email, password };20}2122const user = await prisma.user.findUnique({ where: { email } });23if (!user) {24return { errors: { email: ['User not found'] }, email, password };25}2627const valid = await compare(password, user.password);28if (!valid) {29return { errors: { password: ['Invalid password'] }, email, password };30}3132await setUserId({ userId: user.id });33redirect('/');34}
La fonction setUserId
ajoute un cookie user_session
.
Le Router et les Server Components peuvent ensuite lire cet ID pour afficher la navbar ou les pages protégées.
1'use server';2import { cookies } from 'next/headers';34export async function setUserId({ userId }: { userId: number }) {5cookies().set('user_session', String(userId), {6httpOnly: true,7secure: process.env.NODE_ENV === 'production',8path: '/',9maxAge: 60 * 60 * 24 * 30, // 30 jours10});11}
On répète la même approche pour /register
→ RegisterForm
→ register
(Server Action).
1'use server';23import { redirect } from 'next/navigation';4import { getOptionalUser } from '../server/session';5import { RegisterForm } from '../RegisterForm';67export default async function Register() {8const user = await getOptionalUser();9if (user) redirect('/');10return <RegisterForm />;11}
1'use client';2import { useActionState, useEffect, useRef } from 'react';3import { register } from './server/auth';45export function RegisterForm() {6const [state, formAction, pending] = useActionState(register, {7errors: { email: undefined, password: undefined },8email: '',9password: '',10});11const emailRef = useRef<HTMLInputElement>(null);12const passwordRef = useRef<HTMLInputElement>(null);1314useEffect(() => {15if (state?.errors?.email) emailRef.current?.focus();16else if (state?.errors?.password) passwordRef.current?.focus();17}, [state?.errors]);1819return (20<form action={formAction} className="space-y-4">21{/* similaire à LoginForm, avec labels et validation */}22<button disabled={pending}>23{pending ? 'Signing up...' : 'Sign up'}24</button>25</form>26);27}
1// …2export async function register(prevState: unknown, formData: FormData) {3'use server';4// validation Zod5// check email unique6// hash password avec bcryptjs7// prisma.user.create(...)8// setUserId + redirect('/')9}
useActionState
facilite la gestion des erreurs et du loader.'use server'
) peuvent accéder à Prisma et à cookies()
./profile
en créant un Server Component app/profile/page.tsx
qui affiche l’email et un lien de logout en Server Action.