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 :
<input type="file">
dans un <Form>
RemixCette 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.
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}
<Form>
importé depuis @remix-run/react
encType="multipart/form-data"
indispensable<input>
de type file
avec accept="image/*"
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}
Même pour une page de settings, il faut s’assurer que l’utilisateur est bien connecté avant de rendre le formulaire.
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}
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.
On définit un schéma Zod qui vérifie :
File
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}
z.instanceof(File)
pour réclamer un objet File
superRefine
pour customiser la taille maxZodError
et autres erreursDans le composant React, on récupère :
useActionData
pour le retour serveuruseNavigation
pour savoir si l’upload est en cours1import { 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}
Utilise useNavigation().state
pour bloquer le
bouton “submit” et afficher un spinner avec tes icônes.
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).
<Form encType="multipart/form-data">
<input type="file" accept="image/*">
requireUser()
pour sécuriserunstable_createMemoryUploadHandler
+ unstable_parseMultipartFormData
File
et size
json()
avec feedbackuseActionData
, useNavigation
<AlertFeedback>
pour les erreurs / succèsupdateUserAvatar
envoie sur le bucket.png
et .jpg
.<img src={URL.createObjectURL(file)} />
) de l’avatar sélectionné avant la soumission.