Création d'un composant personnalisé de téléversement de fichiers pour React
L’input natif de fichiers fonctionne, mais il est peu esthétique, inflexible et n’offre aucun retour d’information pendant les téléversements. La plupart des développeurs se tournent vers des bibliothèques comme react-dropzone pour résoudre ce problème. Mais si vous souhaitez un contrôle total—ou simplement moins de dépendances—créer un composant React personnalisé de téléversement de fichiers n’est pas difficile une fois que vous comprenez les contraintes.
Ce guide couvre les éléments essentiels : sélection de fichiers accessible, fonctionnalité de glisser-déposer optionnelle, validation, aperçus, suivi de progression et annulation. Le tout avec des composants fonctionnels modernes et des hooks.
Points clés à retenir
- Les inputs de fichiers sont intrinsèquement non contrôlés dans React en raison des restrictions de sécurité du navigateur qui empêchent l’attribution programmatique de valeurs
- Associez toujours la validation côté client à une validation côté serveur, car les types MIME et l’attribut
acceptpeuvent être contournés - Utilisez
XMLHttpRequestau lieu defetchpour un suivi fiable de la progression du téléversement - Révoquez les URL d’objets créées pour les aperçus de fichiers afin d’éviter les fuites de mémoire
- Le glisser-déposer doit être une amélioration progressive, et non un remplacement des interactions au clavier et par clic
Pourquoi les inputs de fichiers restent non contrôlés
Voici quelque chose qui déroute les développeurs React : vous ne pouvez pas définir la valeur de <input type="file"> de manière programmatique. La sécurité du navigateur l’empêche. Cela signifie que les inputs de fichiers sont intrinsèquement non contrôlés—React peut les lire mais jamais y écrire.
Cela s’applique également aux Form Actions de React 19. Bien que useFormStatus puisse suivre l’état en attente, les Server Actions ne sont pas conçues pour les téléversements de fichiers volumineux. Vous devrez toujours gérer les fichiers côté client avant de les envoyer quelque part.
Les fondations : sélection de fichiers accessible
Commencez par l’input lui-même. Masquez-le visuellement, mais gardez-le accessible :
const FileUploader = () => {
const inputRef = useRef<HTMLInputElement>(null)
const [files, setFiles] = useState<File[]>([])
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
setFiles(Array.from(e.target.files))
}
}
return (
<div>
<input
ref={inputRef}
type="file"
id="file-upload"
className="sr-only"
onChange={handleChange}
multiple
aria-describedby="file-upload-help"
/>
<label htmlFor="file-upload" className="upload-button">
Select files
</label>
<span id="file-upload-help" className="sr-only">
Accepted formats: images, PDFs up to 10MB
</span>
</div>
)
}
L’attribut accept n’est que consultatif—les utilisateurs peuvent le contourner. Validez toujours côté serveur.
Ajout du glisser-déposer comme amélioration progressive
La fonctionnalité de glisser-déposer améliore l’expérience utilisateur mais n’est pas essentielle. Conservez les interactions au clavier et par clic comme parcours principal :
const [isDragging, setIsDragging] = useState(false)
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault()
setIsDragging(true)
}
const handleDragLeave = () => setIsDragging(false)
const handleDrop = (e: React.DragEvent) => {
e.preventDefault()
setIsDragging(false)
const droppedFiles = Array.from(e.dataTransfer.files)
setFiles(droppedFiles)
}
Note : DataTransferItem.getAsFileSystemHandle() offre un accès aux répertoires dans les navigateurs modernes, mais le support reste limité. Restez sur dataTransfer.files pour une compatibilité large.
Validation côté client
Validez avant le téléversement pour améliorer l’expérience utilisateur :
const validateFile = (file: File): string | null => {
const maxSize = 10 * 1024 * 1024 // 10MB
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf']
if (file.size > maxSize) return 'File exceeds 10MB limit'
if (!allowedTypes.includes(file.type)) return 'Invalid file type'
return null
}
Rappel : les types MIME peuvent être falsifiés. La validation côté serveur est obligatoire.
Discover how at OpenReplay.com.
Aperçus de fichiers avec nettoyage approprié
Pour les aperçus d’images, URL.createObjectURL crée des URL temporaires. Révoquez-les pour éviter les fuites de mémoire :
const [preview, setPreview] = useState<string | null>(null)
useEffect(() => {
if (files[0]?.type.startsWith('image/')) {
const url = URL.createObjectURL(files[0])
setPreview(url)
return () => URL.revokeObjectURL(url)
}
}, [files])
Suivi de progression et annulation
Voici une vérité inconfortable : fetch ne prend pas en charge de manière fiable la progression du téléversement. Pour un véritable suivi de progression, utilisez XMLHttpRequest :
const [progress, setProgress] = useState(0)
const xhrRef = useRef<XMLHttpRequest | null>(null)
const upload = (file: File) => {
const xhr = new XMLHttpRequest()
xhrRef.current = xhr
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
setProgress(Math.round((e.loaded / e.total) * 100))
}
}
const formData = new FormData()
formData.append('file', file)
xhr.open('POST', '/api/upload')
xhr.send(formData)
}
const cancel = () => xhrRef.current?.abort()
Pour les fichiers volumineux, envisagez des téléversements directs vers un stockage d’objets (URL présignées S3) ou des approches par morceaux/multipart. Envoyer des fichiers de plusieurs gigaoctets via votre serveur d’application se termine rarement bien.
États d’erreur
Suivez explicitement l’état du téléversement :
type UploadState =
| { status: 'idle' }
| { status: 'uploading'; progress: number }
| { status: 'success'; url: string }
| { status: 'error'; message: string }
Cette union discriminée rend le rendu simple et type-safe.
Quand utiliser une bibliothèque à la place
Créer votre propre composant React personnalisé de téléversement de fichiers a du sens lorsque vous avez besoin d’un comportement spécifique ou souhaitez minimiser la taille du bundle. Des bibliothèques comme react-dropzone gèrent des cas limites que vous pourriez manquer—mais elles ajoutent également des dépendances et des opinions que vous ne souhaitez peut-être pas.
Le modèle de base est simple : input masqué, label accessible, zone de dépôt optionnelle, validation et XHR pour la progression. Tout le reste n’est que raffinement.
Conclusion
Un composant personnalisé de téléversement de fichiers vous donne un contrôle précis sur le comportement, le style et la taille du bundle. Les contraintes clés sont simples : les inputs de fichiers doivent rester non contrôlés, la validation côté client complète mais ne remplace jamais les vérifications côté serveur, et XMLHttpRequest reste le choix fiable pour le suivi de progression. Commencez par des fondations accessibles, ajoutez le glisser-déposer comme amélioration optionnelle, et gérez le nettoyage de la mémoire pour les aperçus. Que vous construisiez sur mesure ou que vous utilisiez une bibliothèque dépend de vos besoins spécifiques—mais maintenant vous comprenez ce qui se passe sous le capot dans les deux cas.
FAQ
La sécurité du navigateur empêche l'attribution programmatique de valeurs sur les inputs de fichiers. Pour réinitialiser, appelez soit inputRef.current.value = '' en utilisant une ref (ce qui fonctionne pour effacer mais pas pour définir), soit démontez et remontez l'input en changeant sa prop key. Les deux approches effacent efficacement la sélection tout en respectant les restrictions du navigateur.
Créez un tableau d'états de téléversement, un par fichier. Chaque objet d'état suit sa propre progression, son statut et toute erreur. Utilisez le nom du fichier ou un ID généré comme clé. Lors du téléversement, parcourez les fichiers et créez des instances XMLHttpRequest séparées, en mettant à jour l'entrée d'état correspondante dans chaque callback de progression.
L'événement dragLeave se déclenche lors du déplacement entre éléments parent et enfant. Corrigez cela en vérifiant si la cible associée est contenue dans la zone de dépôt avant de définir isDragging sur false, ou utilisez un compteur qui s'incrémente sur dragEnter et se décrémente sur dragLeave, ne mettant à jour l'état que lorsque le compteur atteint zéro.
Utilisez FormData dans la plupart des cas. Il gère efficacement les données binaires et prend en charge l'encodage multipart/form-data attendu par les serveurs. L'encodage base64 augmente la taille du fichier d'environ 33% et nécessite plus de traitement des deux côtés. N'utilisez base64 que lorsque votre API l'exige spécifiquement ou lors de l'intégration de petits fichiers dans des charges utiles JSON.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.