Formulaires d’inscription et de connexion Remix
Créez des formulaires ergonomiques pour l’inscription et la connexion avec validation côté client et serveur.
Introduction
Dans cette leçon, on va voir comment mettre en place des formulaires d’inscription et de connexion dans un projet Remix avec un backend Strapi 5. On utilisera :
- Zod pour définir et valider nos schémas de données.
- Conform (
@conform-to/react+@conform-to/zod) pour une gestion ergonomique des formulaires. - Les loaders/actions de Remix et les sessions pour authentifier l’utilisateur.
À l’issue de la leçon, tu sauras :
- Définir un schéma Zod synchronisé et asynchrone.
- Composer
actionRemix pour valider, gérer les erreurs et créer la session. - Afficher les erreurs de validation côté client.
- Structurer un formulaire réutilisable avec Conform.
Login : schéma et action
1. Définir le schéma Zod
On commence par formaliser les champs attendus et les contraintes.
1import { z } from "zod";23export const LoginSchema = z.object({4email: z.string().email("Adresse email invalide"),5password: z.string().min(8, "Le mot de passe doit faire ≥ 8 caractères"),6});
2. Action function Remix
L’action récupère le FormData, l’envoie à Conform/Zod, puis vérifie l’existence de l’utilisateur et la validité du mot de passe via Strapi.
1import { Form, Link, json, useActionData, useNavigation }2from "@remix-run/react";3import type { ActionFunctionArgs } from "@remix-run/node";4import { useForm, getFormProps, getInputProps }5from "@conform-to/react";6import { parseWithZod } from "@conform-to/zod";7import { LoginSchema } from "~/utils/schemas/login.schema";8import { checkIfUserExists, isUserPasswordValid, logUser }9from "~/strapi.server";10import { createUserSession } from "~/sessions.server";1112export const action = async ({ request }: ActionFunctionArgs) => {13const formData = await request.formData();14const submission = await parseWithZod(formData, {15schema: LoginSchema.superRefine(async (data, ctx) => {16const exists = await checkIfUserExists({ email: data.email });17if (!exists) {18ctx.addIssue({19code: "custom",20message: "Utilisateur inconnu.",21path: ["email"],22});23}24const { isPasswordValid } = await isUserPasswordValid({25email: data.email,26currentPassword: data.password,27});28if (!isPasswordValid) {29ctx.addIssue({30code: "custom",31message: "Mot de passe incorrect.",32path: ["password"],33});34}35}),36async: true,37});3839if (submission.status !== "success") {40return json({ result: submission.reply() });41}4243const { jwt } = await logUser({ loginData: submission.value });44return createUserSession({45request,46strapiUserToken: jwt,47});48};
UI du formulaire de connexion
On utilise useForm pour connecter Conform à notre UI. Les erreurs et l’état de soumission sont gérés automatiquement.
1export default function Login() {2const actionData = useActionData<typeof action>();3const navigation = useNavigation();4const isSubmitting = navigation.state === "submitting";56const [form, fields] = useForm({7lastResult: actionData?.result,8onValidate({ formData }) {9return parseWithZod(formData, { schema: LoginSchema });10},11});1213return (14<div className="max-w-md mx-auto mt-8">15<h2 className="text-2xl font-bold mb-4">Connexion</h2>16<Form method="post" {...getFormProps(form)}>17<fieldset disabled={isSubmitting} className="space-y-4">18<div>19<label htmlFor="email">Email</label>20<input21{...getInputProps(fields.email, { type: "email" })}22className="w-full px-3 py-2 border rounded"23/>24{fields.email.errors && (25<p className="text-red-500">{fields.email.errors}</p>26)}27</div>2829<div>30<label htmlFor="password">Mot de passe</label>31<input32{...getInputProps(fields.password, { type: "password" })}33className="w-full px-3 py-2 border rounded"34/>35{fields.password.errors && (36<p className="text-red-500">{fields.password.errors}</p>37)}38</div>3940<button41type="submit"42className="w-full bg-blue-500 text-white py-2 rounded"43>44{isSubmitting ? "Connexion..." : "Se connecter"}45</button>46</fieldset>47</Form>48<p className="mt-4 text-center">49Pas de compte ?{" "}50<Link to="/register" className="text-blue-600 hover:underline">51Inscription52</Link>53</p>54</div>55);56}
Tip
Conform synchronise automatiquement l’état du formulaire et les erreurs
Zod + Conform gèrent validation sync/async & renvoient un objet
reply().
Inscription : schéma, action et UI
1. Schéma Zod pour l’inscription
On ajoute la confirmation de mot de passe et on vérifie l’unicité de l’email.
1import { z } from "zod";23export const RegisterSchema = z4.object({5email: z.string().email("Email invalide"),6password: z.string().min(8, "8 caractères min."),7confirmPassword: z.string(),8})9.refine((data) => data.password === data.confirmPassword, {10message: "Les mots de passe doivent correspondre",11path: ["confirmPassword"],12});
2. Action function pour l’inscription
On s’assure qu’aucun user n’existe déjà, puis on crée l’utilisateur dans Strapi.
1import { Form, useActionData, useNavigation, Link }2from "@remix-run/react";3import { useForm, getFormProps, getInputProps }4from "@conform-to/react";5import { parseWithZod } from "@conform-to/zod";6import type { ActionFunctionArgs } from "@remix-run/node";7import { RegisterSchema } from "~/utils/schemas/register.schema";8import { createUserInStrapi } from "~/strapi.server";9import { createUserSession } from "~/sessions.server";1011export const action = async ({ request }: ActionFunctionArgs) => {12const formData = await request.formData();13const submission = await parseWithZod(formData, {14schema: RegisterSchema.superRefine(async (data, ctx) => {15const exists = await checkIfUserExists({ email: data.email });16if (exists) {17ctx.addIssue({18code: "custom",19message: "Email déjà utilisé",20path: ["email"],21});22}23}),24async: true,25});2627if (submission.status !== "success") {28return json({ result: submission.reply() });29}3031const user = await createUserInStrapi({32email: submission.value.email,33password: submission.value.password,34});3536return createUserSession({37request,38strapiUserToken: user.jwt,39});40};
3. UI du formulaire d’inscription
1export default function Register() {2const actionData = useActionData<typeof action>();3const navigation = useNavigation();4const isSubmitting = navigation.state === "submitting";56const [form, fields] = useForm({7lastResult: actionData?.result,8onValidate({ formData }) {9return parseWithZod(formData, { schema: RegisterSchema });10},11});1213return (14<div className="max-w-md mx-auto mt-8">15<h2 className="text-2xl font-bold mb-4">Inscription</h2>16<Form method="post" {...getFormProps(form)}>17<fieldset disabled={isSubmitting} className="space-y-4">18<div>19<label>Email</label>20<input21{...getInputProps(fields.email, { type: "email" })}22className="w-full px-3 py-2 border rounded"23/>24{fields.email.errors && (25<p className="text-red-500">{fields.email.errors}</p>26)}27</div>2829<div>30<label>Mot de passe</label>31<input32{...getInputProps(fields.password, { type: "password" })}33className="w-full px-3 py-2 border rounded"34/>35</div>3637<div>38<label>Confirme mot de passe</label>39<input40{...getInputProps(fields.confirmPassword, { type: "password" })}41className="w-full px-3 py-2 border rounded"42/>43{fields.confirmPassword.errors && (44<p className="text-red-500">45{fields.confirmPassword.errors}46</p>47)}48</div>4950<button51type="submit"52className="w-full bg-green-500 text-white py-2 rounded"53>54{isSubmitting ? "Inscription..." : "S’inscrire"}55</button>56</fieldset>57</Form>58<p className="mt-4 text-center">59Déjà un compte ?{" "}60<Link to="/login" className="text-blue-600 hover:underline">61Connexion62</Link>63</p>64</div>65);66}
Mot de passe côté client
Toujours transmettre le mot de passe en HTTPS et ne jamais le stocker en clair.
Points clés
- Zod + Conform gèrent à la fois les validations synchrones et asynchrones.
parseWithZod(formData, {...})renvoie un objetsubmissionau format standard.- Utilise
fields.xxx.errorspour afficher les erreurs sous chaque champ. - Crée la session avec
createUserSessionaprès un login/inscription réussi. - Conform simplifie la liaison entre ton UI React et la validation Zod sans boilerplate.
Backlinks utiles
- Remix docs actions/loaders & forms
- React Router formulaires
- Zod GitHub
- Conform GitHub
- Strapi 5 auth
Exercices rapides
- Crée un formulaire “Mot de passe oublié” avec un schéma Zod qui valide l’email.
- Ajoute un champ
usernameà l’inscription, assure-toi qu’il est unique en backend. - Personnalise la page de connexion pour rediriger l’utilisateur vers
/dashboard
après réussite de la connexion.