Formulaires d’inscription et de connexion
Crée des formulaires modernes, gère la validation et l’état avec Zod et useActionState.
Accéder gratuitement à cette formation
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 :
- Comment structurer des React Server Components pour protéger l’accès.
- L’utilisation des Server Actions (
use server) pour traiter les formulaires côté serveur. - Le hook useActionState pour gérer l’état, les erreurs et le loader côté client.
- Les fonctions Prisma pour créer et authentifier des utilisateurs.
Présentation générale
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.
Tip
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.
Création de la page login
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}
Composant LoginForm (client)
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}
Pourquoi useActionState ?
🔄 useActionState permet de piloter la soumission, récupérer les erreurs
validées par Zod et afficher un loader automatiquement.
Ajout de la logique serveur pour login
Dans app/server/auth.ts, on définit la fonction login marquée avec "use server". Elle :
- Valide les champs avec Zod.
- Vérifie l’existence de l’utilisateur dans Prisma.
- Compare les mots de passe (bcryptjs).
- Crée un cookie via
setUserId. - Effectue la redirection.
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}
Gestion de la session
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}
Création du formulaire d’inscription
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}
Points clés à retenir
- Les Server Components (pages) restent statiques côté client : tout hook se fait dans un Client Component.
useActionStatefacilite la gestion des erreurs et du loader.- Les Server Actions (
'use server') peuvent accéder à Prisma et àcookies(). - La validation avec Zod garantit la cohérence des données.
- L’authentification repose sur un cookie HTTP-only : le cookie + Server Components protègent tes routes.
Exercices
- Ajouter un champ “username” au formulaire d’inscription et au modèle Prisma, puis rafraîchir la navbar pour afficher ce nom d’utilisateur.
- Protéger la page
/profileen créant un Server Componentapp/profile/page.tsxqui affiche l’email et un lien de logout en Server Action. - Implémenter un formulaire “forgot password” qui envoie un token par e-mail (mock).