Upload d’avatar côté Remix et validation du fichier

Formulaire d’upload, validation de la taille/type, feedback utilisateur, preview de l’avatar.

5 min read

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.

app/routes/settings/avatar.tsx
1
import { Form } from '@remix-run/react'
2
import { Label } from '~/components/ui/label.tsx'
3
import { Input } from '~/components/ui/input.tsx'
4
import { Button } from '~/components/ui/button.tsx'
5
6
export default function AvatarSettings() {
7
return (
8
<Form method="post" encType="multipart/form-data">
9
<div className="grid gap-2">
10
<Label htmlFor="avatar">Ton avatar</Label>
11
<Input
12
id="avatar"
13
name="avatar"
14
type="file"
15
accept="image/*"
16
required
17
/>
18
<Button type="submit">Modifier l’avatar</Button>
19
</div>
20
</Form>
21
)
22
}

points clés

  • <Form> importé depuis @remix-run/react
  • encType="multipart/form-data" indispensable
  • champ <input> de type file avec accept="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.

app/routes/settings/avatar.tsx
1
export const loader = async ({ request }) => {
2
await requireUser({ request }) // ↪️ vérification auth
3
return json({})
4
}

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).

app/routes/settings/avatar.tsx
1
import {
2
unstable_createMemoryUploadHandler,
3
unstable_parseMultipartFormData,
4
} from '@remix-run/node'
5
6
const MAX_FILE_SIZE = 10_000_000 // 10 Mo
7
8
export const action = async ({ request }) => {
9
await requireUser({ request })
10
11
const uploadHandler = unstable_createMemoryUploadHandler({
12
maxPartSize: MAX_FILE_SIZE,
13
})
14
15
const formData = await unstable_parseMultipartFormData(
16
request,
17
uploadHandler,
18
)
19
20
// … suite dans la section validation
21
}

3.2 valider avec Zod

On définit un schéma Zod qui vérifie :

  1. Le champ est bien un File
  2. La taille ne dépasse pas MAX_FILE_SIZE
utils/validation.ts
1
import { z } from 'zod'
2
3
export const UpdateAvatarSchema = z.object({
4
avatar: z.instanceof(File).superRefine((file, ctx) => {
5
if (file.size > MAX_FILE_SIZE) {
6
ctx.addIssue({
7
code: z.ZodIssueCode.custom,
8
message: "L'image est trop lourde (10 Mo max)",
9
})
10
return false
11
}
12
return true
13
}),
14
})

Puis, dans l’action, on appelle :

app/routes/settings/avatar.tsx
1
import { UpdateAvatarSchema } from '~/utils/validation.ts'
2
import { json } from '@remix-run/node'
3
4
export const action = async ({ request }) => {
5
// ... après avoir obtenu formData
6
7
try {
8
UpdateAvatarSchema.parse({
9
avatar: formData.get('avatar'),
10
})
11
12
// appel au service d’upload vers S3
13
const feedback = await updateUserAvatar({ request, formData })
14
return json(feedback)
15
} catch (error) {
16
if (error instanceof z.ZodError) {
17
return json({ error: true, message: error.errors
18
.map(e => e.message).join('\n') })
19
}
20
return json({ error: true, message: error.message ||
21
"Erreur lors de l’upload" })
22
}
23
}

points clés

  • z.instanceof(File) pour réclamer un objet File
  • superRefine pour customiser la taille max
  • distinction entre ZodError et autres erreurs

4. gérer le feedback utilisateur

Dans le composant React, on récupère :

  • useActionData pour le retour serveur
  • useNavigation pour savoir si l’upload est en cours
app/routes/settings/avatar.tsx
1
import { useActionData, useNavigation, Form } from '@remix-run/react'
2
import { AlertFeedback } from '~/components/FeedbackComponent.tsx'
3
import { useUser } from '~/root.tsx'
4
import { Icons } from '~/components/icons.tsx'
5
6
export default function AvatarSettings() {
7
const user = useUser()
8
const feedback = useActionData()
9
const nav = useNavigation()
10
const isLoading = nav.state !== 'idle'
11
12
return (
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" />}
19
Modifier l’avatar
20
</button>
21
</Form>
22
)
23
}

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

  1. Formulaire
    • <Form encType="multipart/form-data">
    • <input type="file" accept="image/*">
  2. Loader
    • requireUser() pour sécuriser
  3. Action
    • unstable_createMemoryUploadHandler + unstable_parseMultipartFormData
    • Validation Zod sur File et size
    • Retour json() avec feedback
  4. UI
    • useActionData, useNavigation
    • <AlertFeedback> pour les erreurs / succès
  5. Service S3
    • updateUserAvatar envoie sur le bucket

Exercises

  1. Adapter la validation pour n’accepter que les extensions .png et .jpg.
  2. Afficher un aperçu (<img src={URL.createObjectURL(file)} />) de l’avatar sélectionné avant la soumission.
  3. 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.