Création des formulaires, validation avec Zod, feedback utilisateur et gestion des erreurs.
Dans cette leçon, tu vas apprendre à :
Cette approche s’appuie sur les notions de
- loaders et actions Remix
- Forms Remix (docs)
- Validation avec Zod
- Session JWT &
authenticateUser()
/register
)loader
pour rediriger un utilisateur déjà connecté.action
pour :
/auth/register
).authenticateUser
).Préfère schema.safeParse()
pour récupérer un objet error
sans throw.
1import type { LoaderFunctionArgs, ActionFunctionArgs } from '@remix-run/node'2import { json, redirect } from '@remix-run/node'3import { Form, useActionData, useNavigation } from '@remix-run/react'4import { z } from 'zod'5import { getOptionalUser } from '~/auth.server.ts'6import { fetcher } from '~/server/utils.server.ts'7import { authenticateUser } from '~/session.server.ts'8import { AlertFeedback } from '~/components/FeedbackComponent.tsx'910// 1. Schéma Zod pour valider l’input11const registerSchema = z.object({12email: z.string().email({ message: 'Email invalide' }),13password: z14.string().min(6, { message: 'Min 6 caractères' }),15firstName: z16.string().min(2, { message: 'Min 2 caractères' }),17})1819export const loader = async ({ request }: LoaderFunctionArgs) => {20const user = await getOptionalUser({ request })21if (user) {22return redirect('/') // Utilisateur déjà connecté23}24return json({})25}2627export const action = async ({ request }: ActionFunctionArgs) => {28try {29const formData = await request.formData() // ^? formData30const data = Object.fromEntries(formData)31const parsed = registerSchema.safeParse(data)3233if (!parsed.success) {34const msg = parsed.error.errors.map(e => e.message).join(', ')35return json({ error: true, message: msg })36}3738// Appel API NestJS39const res = await fetcher({40url: '/auth/register',41method: 'POST',42data: parsed.data,43request,44})45const { access_token, error, message } = z46.object({47access_token: z.string().optional(),48error: z.boolean().optional(),49message: z.string().optional(),50})51.parse(res)5253if (error) {54return json({ error, message })55}56if (access_token) {57return authenticateUser({ request, userToken: access_token })58}59throw new Error('Erreur inattendue')60} catch (err: any) {61return json({ error: true, message: err.message })62}63}6465export default function RegisterForm() {66const feedback = useActionData()67const nav = useNavigation()68const isLoading = nav.state !== 'idle'6970return (71<Form method="POST" className="space-y-4">72<input73name="firstName"74placeholder="Prénom"75required76disabled={isLoading}77/>78<input79name="email"80type="email"81placeholder="Email"82required83disabled={isLoading}84/>85<input86name="password"87type="password"88placeholder="Mot de passe"89required90disabled={isLoading}91/>92<AlertFeedback feedback={feedback} />93<button type="submit">94{isLoading ? 'Chargement…' : "S'inscrire"}95</button>96</Form>97)98}
loader
protège la route pour un utilisateur déjà connecté.action
gère la logique “submit” : parse + appel API + session.useNavigation()
indique l’état de loading pour désactiver les champs./
)Cette route affiche soit :
1import type { LoaderFunctionArgs, ActionFunctionArgs } from '@remix-run/node'2import { json } from '@remix-run/node'3import { Form, useActionData, useLoaderData, useNavigation } from '@remix-run/react'4import { z } from 'zod'5import { getOptionalUser } from '~/auth.server.ts'6import { getConversations } from '~/server/chat.server.ts'7import { getUsers } from '~/server/user.server.ts'8import { fetcher } from '~/server/utils.server.ts'9import { authenticateUser } from '~/session.server.ts'10import { AlertFeedback } from '~/components/FeedbackComponent.tsx'1112// Schéma pour login13const loginSchema = z.object({14email: z.string().email(),15password: z.string(),16})1718export const loader = async ({ request }: LoaderFunctionArgs) => {19const user = await getOptionalUser({ request })20if (user) {21const users = await getUsers({ request })22const convos = await getConversations({ request })23return json({ users, convos })24}25return json({ users: [], convos: [] })26}2728export const action = async ({ request }: ActionFunctionArgs) => {29try {30const formData = await request.formData()31const data = Object.fromEntries(formData)32const parsed = loginSchema.parse(data)3334// Appel direct à l’API NestJS35const response = await fetch(`${process.env.BACKEND_URL}/auth/login`, {36method: 'POST',37headers: { 'Content-Type': 'application/json' },38body: JSON.stringify(parsed),39})40const body = await response.json()41const { access_token, error, message } = z.object({42access_token: z.string().optional(),43error: z.boolean().optional(),44message: z.string().optional(),45}).parse(body)4647if (error) return json({ error, message })48if (access_token) {49return authenticateUser({ request, userToken: access_token })50}51throw new Error('Échec de login')52} catch (err: any) {53return json({ error: true, message: err.message })54}55}5657export default function Index() {58const { users, convos } = useLoaderData()59const feedback = useActionData()60const nav = useNavigation()61const isLoading = nav.state !== 'idle'6263return users.length ? (64<Conversations users={users} conversations={convos} />65) : (66<Form method="POST" className="space-y-4">67<input68name="email"69type="email"70placeholder="Email"71required72disabled={isLoading}73/>74<input75name="password"76type="password"77placeholder="Mot de passe"78required79disabled={isLoading}80/>81<AlertFeedback feedback={feedback} />82<button type="submit">83{isLoading ? '...' : 'Se connecter'}84</button>85</Form>86)87}
Dans un vrai chat temps réel avec Socket.io, tu pourras lancer la connexion
dans un useEffect
après récupération du token en session.
zod.safeParse()
vs zod.parse()
.authenticateUser()
écrit le JWT en cookieuseNavigation()
et useActionData()
.<Form>
+ Radix UI + Tailwind CSSVoir aussi le module suivant pour stocker tes avatars sur AWS S3
et gérer tes dons avec Stripe.
lastName
au formulaire d’inscription
registerSchema
et la UI./logout
avec une simple action qui détruit la session.Bon code ! 🚀