Upload d’avatar côté Remix et validation du fichier
Formulaire d’upload, validation de la taille/type, feedback utilisateur, preview de l’avatar.
Dans cette leçon, tu vas apprendre à gérer l’upload d’avatars côté Remix, valider le fichier avant envoi, puis déléguer le stockage à AWS S3 via une fonction updateUserAvatar.
On part du principe que tu disposes déjà d’un système d’authentification, d’un composant <AlertFeedback> et d’un hook useUser().
À la fin, tu seras capable de :
- Proposer un champ
<input type="file">dans un<Form>Remix - Parse et valider les données multipart en mémoire
- Vérifier le type et la taille du fichier via Zod
- Gérer les erreurs et afficher un feedback utilisateur
- Appeler un service server-side pour envoyer l’image vers AWS S3
Cette leçon fait partie du module « Gestion des avatars utilisateurs avec AWS S3 » de la formation Développe une app de chat temps réel avec Remix et NestJS.
1. créer le formulaire d’upload
Commence par intégrer un <Form> Remix avec un champ fichier. N’oublie pas encType="multipart/form-data" pour que le navigateur envoie bien les blobs.
1import { Form } from '@remix-run/react'2import { Label } from '~/components/ui/label.tsx'3import { Input } from '~/components/ui/input.tsx'4import { Button } from '~/components/ui/button.tsx'56export default function AvatarSettings() {7return (8<Form method="post" encType="multipart/form-data">9<div className="grid gap-2">10<Label htmlFor="avatar">Ton avatar</Label>11<Input12id="avatar"13name="avatar"14type="file"15accept="image/*"16required17/>18<Button type="submit">Modifier l’avatar</Button>19</div>20</Form>21)22}
points clés
<Form>importé depuis@remix-run/reactencType="multipart/form-data"indispensable- champ
<input>de typefileavecaccept="image/*"
2. loader : sécuriser l’accès
Dans le loader, on s’assure que l’utilisateur est authentifié via requireUser() – une simple guard qui lève si pas de session.
1export const loader = async ({ request }) => {2await requireUser({ request }) // ↪️ vérification auth3return json({})4}
Pourquoi protéger le loader ?
Même pour une page de settings, il faut s’assurer que l’utilisateur est bien connecté avant de rendre le formulaire.
3. action : parser et valider le fichier
3.1 créer un memory upload handler
On utilise l’API expérimentale de Remix pour parser le FormData en mémoire (maxPartSize fixe la taille max autorisée).
1import {2unstable_createMemoryUploadHandler,3unstable_parseMultipartFormData,4} from '@remix-run/node'56const MAX_FILE_SIZE = 10_000_000 // 10 Mo78export const action = async ({ request }) => {9await requireUser({ request })1011const uploadHandler = unstable_createMemoryUploadHandler({12maxPartSize: MAX_FILE_SIZE,13})1415const formData = await unstable_parseMultipartFormData(16request,17uploadHandler,18)1920// … suite dans la section validation21}
Attention à la mémoire
Un memory upload handler stocke l’image dans la RAM. Pour de gros fichiers (> 10 Mo), préfère un stream vers S3 ou un handler sur disque.
3.2 valider avec Zod
On définit un schéma Zod qui vérifie :
- Le champ est bien un
File - La taille ne dépasse pas
MAX_FILE_SIZE
1import { z } from 'zod'23export const UpdateAvatarSchema = z.object({4avatar: z.instanceof(File).superRefine((file, ctx) => {5if (file.size > MAX_FILE_SIZE) {6ctx.addIssue({7code: z.ZodIssueCode.custom,8message: "L'image est trop lourde (10 Mo max)",9})10return false11}12return true13}),14})
Puis, dans l’action, on appelle :
1import { UpdateAvatarSchema } from '~/utils/validation.ts'2import { json } from '@remix-run/node'34export const action = async ({ request }) => {5// ... après avoir obtenu formData67try {8UpdateAvatarSchema.parse({9avatar: formData.get('avatar'),10})1112// appel au service d’upload vers S313const feedback = await updateUserAvatar({ request, formData })14return json(feedback)15} catch (error) {16if (error instanceof z.ZodError) {17return json({ error: true, message: error.errors18.map(e => e.message).join('\n') })19}20return json({ error: true, message: error.message ||21"Erreur lors de l’upload" })22}23}
points clés
z.instanceof(File)pour réclamer un objetFilesuperRefinepour customiser la taille max- distinction entre
ZodErroret autres erreurs
4. gérer le feedback utilisateur
Dans le composant React, on récupère :
useActionDatapour le retour serveuruseNavigationpour savoir si l’upload est en cours
1import { useActionData, useNavigation, Form } from '@remix-run/react'2import { AlertFeedback } from '~/components/FeedbackComponent.tsx'3import { useUser } from '~/root.tsx'4import { Icons } from '~/components/icons.tsx'56export default function AvatarSettings() {7const user = useUser()8const feedback = useActionData()9const nav = useNavigation()10const isLoading = nav.state !== 'idle'1112return (13<Form method="post" encType="multipart/form-data">14{user.avatarUrl && <img src={user.avatarUrl} alt="avatar" />}15{/* ...input file */}16<AlertFeedback feedback={feedback} />17<button disabled={isLoading}>18{isLoading && <Icons.spinner className="animate-spin" />}19Modifier l’avatar20</button>21</Form>22)23}
Tip
Utilise useNavigation().state pour bloquer le
bouton “submit” et afficher un spinner avec tes icônes.
5. envoi vers AWS S3
Le coeur du stockage se trouve dans updateUserAvatar, côté server.
Cette fonction extrait le blob formData.get('avatar'), génère un nom de fichier unique, puis fait un putObject vers S3.
Voir la leçon suivante pour la mise en place du bucket, des permissions IAM, et la clef d’accès AWS (Access Key / Secret).
Récapitulatif
- Formulaire
<Form encType="multipart/form-data"><input type="file" accept="image/*">
- Loader
requireUser()pour sécuriser
- Action
unstable_createMemoryUploadHandler+unstable_parseMultipartFormData- Validation Zod sur
Fileetsize - Retour
json()avec feedback
- UI
useActionData,useNavigation<AlertFeedback>pour les erreurs / succès
- Service S3
updateUserAvatarenvoie sur le bucket
Exercises
- Adapter la validation pour n’accepter que les extensions
.pnget.jpg. - Afficher un aperçu (
<img src={URL.createObjectURL(file)} />) de l’avatar sélectionné avant la soumission. - Mettre en place un handler qui stream directement l’upload vers S3 (sans passer par la mémoire), et mesurer la différence de consommation RAM.