Back

Construindo um Componente Personalizado de Upload de Arquivos para React

Construindo um Componente Personalizado de Upload de Arquivos para React

O input de arquivo nativo funciona, mas é feio, inflexível e não oferece nenhum feedback durante os uploads. A maioria dos desenvolvedores recorre a bibliotecas como react-dropzone para resolver isso. Mas se você quer controle total—ou apenas menos dependências—construir um componente personalizado de upload de arquivos em React não é difícil uma vez que você entende as restrições.

Este guia cobre o essencial: seleção de arquivos acessível, funcionalidade opcional de arrastar e soltar, validação, pré-visualizações, acompanhamento de progresso e cancelamento. Tudo com componentes funcionais modernos e hooks.

Pontos-Chave

  • Inputs de arquivo são inerentemente não controlados no React devido a restrições de segurança do navegador que impedem a atribuição programática de valores
  • Sempre combine validação client-side com validação server-side, já que tipos MIME e o atributo accept podem ser contornados
  • Use XMLHttpRequest em vez de fetch para acompanhamento confiável do progresso de upload
  • Revogue URLs de objeto criadas para pré-visualizações de arquivos para evitar vazamentos de memória
  • Arrastar e soltar deve ser um aprimoramento progressivo, não uma substituição para interações por teclado e clique

Por que inputs de arquivo permanecem não controlados

Aqui está algo que confunde desenvolvedores React: você não pode definir o valor de <input type="file"> programaticamente. A segurança do navegador impede isso. Isso significa que inputs de arquivo são inerentemente não controlados—React pode ler deles, mas nunca escrever neles.

Isso também importa para as Form Actions do React 19. Embora useFormStatus possa rastrear o estado pendente, Server Actions não foram projetadas para uploads de arquivos grandes. Você ainda vai lidar com arquivos no client-side antes de enviá-los para qualquer lugar.

A fundação: seleção de arquivos acessível

Comece com o input em si. Esconda-o visualmente, mas mantenha-o acessível:

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

O atributo accept é apenas consultivo—usuários podem contorná-lo. Sempre valide no server-side.

Adicionando arrastar e soltar como aprimoramento progressivo

A funcionalidade de upload de arquivos por arrastar e soltar melhora a UX, mas não é essencial. Mantenha as interações por teclado e clique como o caminho 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() oferece acesso a diretórios em navegadores modernos, mas o suporte permanece limitado. Mantenha-se com dataTransfer.files para compatibilidade ampla.

Validação client-side

Valide antes do upload para melhorar a experiência do usuário:

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
}

Lembre-se: tipos MIME podem ser falsificados. Validação server-side é obrigatória.

Pré-visualizações de arquivos com limpeza adequada

Para pré-visualizações de imagens, URL.createObjectURL cria URLs temporárias. Revogue-as para evitar vazamentos de memória:

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

Acompanhamento de progresso e cancelamento

Aqui está a verdade desconfortável: fetch não suporta progresso de upload de forma confiável. Para acompanhamento real de progresso, use 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 arquivos grandes, considere uploads diretos para armazenamento de objetos (URLs pré-assinadas do S3) ou abordagens fragmentadas/multipart. Enviar arquivos de vários gigabytes através do seu servidor de aplicação raramente termina bem.

Estados de erro

Rastreie o estado de upload explicitamente:

type UploadState = 
  | { status: 'idle' }
  | { status: 'uploading'; progress: number }
  | { status: 'success'; url: string }
  | { status: 'error'; message: string }

Esta união discriminada torna a renderização direta e type-safe.

Quando usar uma biblioteca em vez disso

Construir seu próprio componente personalizado de upload de arquivos em React faz sentido quando você precisa de comportamento específico ou quer minimizar o tamanho do bundle. Bibliotecas como react-dropzone lidam com casos extremos que você pode perder—mas também adicionam dependências e opiniões que você pode não querer.

O padrão central é simples: input oculto, label acessível, zona de soltar opcional, validação e XHR para progresso. Todo o resto é refinamento.

Conclusão

Um componente personalizado de upload de arquivos oferece controle preciso sobre comportamento, estilização e tamanho do bundle. As restrições principais são diretas: inputs de arquivo devem permanecer não controlados, validação client-side complementa mas nunca substitui verificações server-side, e XMLHttpRequest permanece a escolha confiável para acompanhamento de progresso. Comece com fundações acessíveis, adicione arrastar e soltar como uma melhoria opcional e lide com limpeza de memória para pré-visualizações. Se você constrói personalizado ou recorre a uma biblioteca depende de suas necessidades específicas—mas agora você entende o que está acontecendo por baixo dos panos de qualquer forma.

Perguntas Frequentes

A segurança do navegador impede a atribuição programática de valores em inputs de arquivo. Para redefinir, ou chame inputRef.current.value = '' usando uma ref (que funciona para limpar mas não para definir), ou desmonte e remonte o input alterando sua prop key. Ambas as abordagens efetivamente limpam a seleção enquanto respeitam as restrições do navegador.

Crie um array de estados de upload, um por arquivo. Cada objeto de estado rastreia seu próprio progresso, status e quaisquer erros. Use o nome do arquivo ou um ID gerado como chave. Ao fazer upload, mapeie sobre os arquivos e crie instâncias separadas de XMLHttpRequest, atualizando a entrada de estado correspondente em cada callback de progresso.

O evento dragLeave dispara ao mover entre elementos pai e filho. Corrija isso verificando se o alvo relacionado está contido dentro da zona de soltar antes de definir isDragging como false, ou use um contador que incrementa em dragEnter e decrementa em dragLeave, atualizando o estado apenas quando o contador chegar a zero.

Use FormData para a maioria dos casos. Ele lida com dados binários de forma eficiente e suporta a codificação multipart/form-data que os servidores esperam. A codificação base64 aumenta o tamanho do arquivo em aproximadamente 33% e requer mais processamento em ambos os lados. Use base64 apenas quando sua API especificamente exigir ou ao incorporar arquivos pequenos em 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.

OpenReplay