Construcción de un Componente Personalizado de Carga de Archivos para React
El input de archivo nativo funciona, pero es poco atractivo, inflexible y no ofrece ninguna retroalimentación durante las cargas. La mayoría de los desarrolladores recurren a bibliotecas como react-dropzone para resolver esto. Pero si deseas control total—o simplemente menos dependencias—construir un componente personalizado de carga de archivos en React no es difícil una vez que comprendes las restricciones.
Esta guía cubre lo esencial: selección de archivos accesible, funcionalidad opcional de arrastrar y soltar, validación, vistas previas, seguimiento de progreso y cancelación. Todo con componentes funcionales modernos y hooks.
Puntos Clave
- Los inputs de archivo son inherentemente no controlados en React debido a restricciones de seguridad del navegador que impiden la asignación programática de valores
- Siempre combina la validación del lado del cliente con la validación del lado del servidor, ya que los tipos MIME y el atributo
acceptpueden ser evadidos - Usa
XMLHttpRequesten lugar defetchpara un seguimiento confiable del progreso de carga - Revoca las URLs de objeto creadas para vistas previas de archivos para prevenir fugas de memoria
- Arrastrar y soltar debe ser una mejora progresiva, no un reemplazo para las interacciones de teclado y clic
Por qué los inputs de archivo permanecen no controlados
Aquí hay algo que confunde a los desarrolladores de React: no puedes establecer el valor de <input type="file"> programáticamente. La seguridad del navegador lo impide. Esto significa que los inputs de archivo son inherentemente no controlados—React puede leer de ellos pero nunca escribir en ellos.
Esto también importa para las Form Actions de React 19. Aunque useFormStatus puede rastrear el estado pendiente, las Server Actions no están diseñadas para cargas de archivos grandes. Aún manejarás los archivos del lado del cliente antes de enviarlos a cualquier lugar.
La base: selección de archivos accesible
Comienza con el input en sí. Ocúltalo visualmente, pero mantenlo accesible:
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>
)
}
El atributo accept es solo consultivo—los usuarios pueden evadirlo. Siempre valida del lado del servidor.
Añadiendo arrastrar y soltar como mejora progresiva
La funcionalidad de carga de archivos mediante arrastrar y soltar mejora la experiencia de usuario pero no es esencial. Mantén las interacciones de teclado y clic como la ruta 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)
}
Nota: DataTransferItem.getAsFileSystemHandle() ofrece acceso a directorios en navegadores modernos, pero el soporte sigue siendo limitado. Mantente con dataTransfer.files para una compatibilidad amplia.
Validación del lado del cliente
Valida antes de cargar para mejorar la experiencia del usuario:
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
}
Recuerda: los tipos MIME pueden ser falsificados. La validación del lado del servidor es obligatoria.
Discover how at OpenReplay.com.
Vistas previas de archivos con limpieza adecuada
Para vistas previas de imágenes, URL.createObjectURL crea URLs temporales. Revócalas para prevenir fugas de memoria:
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])
Seguimiento de progreso y cancelación
Aquí está la verdad incómoda: fetch no soporta el progreso de carga de manera confiable. Para un seguimiento de progreso real, usa 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()
Para archivos grandes, considera cargas directas a almacenamiento de objetos (URLs prefirmadas de S3) o enfoques fragmentados/multiparte. Enviar archivos de varios gigabytes a través de tu servidor de aplicaciones rara vez termina bien.
Estados de error
Rastrea el estado de carga explícitamente:
type UploadState =
| { status: 'idle' }
| { status: 'uploading'; progress: number }
| { status: 'success'; url: string }
| { status: 'error'; message: string }
Esta unión discriminada hace que el renderizado sea directo y seguro en tipos.
Cuándo usar una biblioteca en su lugar
Construir tu propio componente personalizado de carga de archivos en React tiene sentido cuando necesitas un comportamiento específico o quieres minimizar el tamaño del bundle. Bibliotecas como react-dropzone manejan casos extremos que podrías pasar por alto—pero también añaden dependencias y opiniones que podrías no querer.
El patrón central es simple: input oculto, etiqueta accesible, zona de soltar opcional, validación y XHR para el progreso. Todo lo demás es refinamiento.
Conclusión
Un componente personalizado de carga de archivos te da control preciso sobre el comportamiento, estilo y tamaño del bundle. Las restricciones clave son directas: los inputs de archivo deben permanecer no controlados, la validación del lado del cliente complementa pero nunca reemplaza las verificaciones del lado del servidor, y XMLHttpRequest sigue siendo la opción confiable para el seguimiento de progreso. Comienza con fundamentos accesibles, añade arrastrar y soltar como una mejora opcional, y maneja la limpieza de memoria para las vistas previas. Si construyes algo personalizado o recurres a una biblioteca depende de tus necesidades específicas—pero ahora comprendes qué está sucediendo bajo el capó de cualquier manera.
Preguntas Frecuentes
La seguridad del navegador impide la asignación programática de valores en inputs de archivo. Para reiniciar, o llama inputRef.current.value = '' usando una ref (lo cual funciona para limpiar pero no para establecer), o desmonta y vuelve a montar el input cambiando su prop key. Ambos enfoques efectivamente limpian la selección mientras respetan las restricciones del navegador.
Crea un array de estados de carga, uno por archivo. Cada objeto de estado rastrea su propio progreso, estado y cualquier error. Usa el nombre del archivo o un ID generado como clave. Al cargar, mapea sobre los archivos y crea instancias separadas de XMLHttpRequest, actualizando la entrada de estado correspondiente en cada callback de progreso.
El evento dragLeave se dispara al moverse entre elementos padre e hijo. Soluciona esto verificando si el objetivo relacionado está contenido dentro de la zona de soltar antes de establecer isDragging a false, o usa un contador que incrementa en dragEnter y decrementa en dragLeave, actualizando el estado solo cuando el contador llega a cero.
Usa FormData para la mayoría de los casos. Maneja datos binarios eficientemente y soporta la codificación multipart/form-data que los servidores esperan. La codificación base64 aumenta el tamaño del archivo aproximadamente en un 33% y requiere más procesamiento en ambos extremos. Solo usa base64 cuando tu API lo requiera específicamente o al incrustar archivos pequeños en payloads 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.