React & Frontend

Upload de Arquivos em React: Preview, Progress e Validação de Tipo na Prática

11 min de leitura

Upload de Arquivos em React: Preview, Progress e Validação de Tipo na Prática

Upload de Arquivos em React: Fundamentos e Arquitetura Upload de arquivos é uma das operações mais comuns em aplicações web modernas. Em React, esse processo envolve não apenas enviar o arquivo para o servidor, mas também proporcionar ao usuário uma experiência visual clara com preview, indicador de progresso e validação robusta. A maioria dos projetos implementa essas funcionalidades de forma amadora, negligenciando casos extremos e criando aplicações frágeis. A estratégia correta começa entendendo que upload não é responsabilidade apenas do React. Seu papel é gerenciar o estado visual e a interação do usuário, enquanto a chamada HTTP (geralmente via ou ) executa o envio real. O React deve manter sincronizados: o arquivo selecionado, a visualização prévia, o status do envio e possíveis erros. Para isso, usaremos hooks como e , além de técnicas nativas de JavaScript para manipulação de files. Vamos construir um componente robusto do zero, entendendo cada decisão arquitetural. Começaremos com a validação de tipo, depois adicionaremos preview

<h2>Upload de Arquivos em React: Fundamentos e Arquitetura</h2>

<p>Upload de arquivos é uma das operações mais comuns em aplicações web modernas. Em React, esse processo envolve não apenas enviar o arquivo para o servidor, mas também proporcionar ao usuário uma experiência visual clara com preview, indicador de progresso e validação robusta. A maioria dos projetos implementa essas funcionalidades de forma amadora, negligenciando casos extremos e criando aplicações frágeis.</p>

<p>A estratégia correta começa entendendo que upload não é responsabilidade apenas do React. Seu papel é gerenciar o estado visual e a interação do usuário, enquanto a chamada HTTP (geralmente via <code>fetch</code> ou <code>axios</code>) executa o envio real. O React deve manter sincronizados: o arquivo selecionado, a visualização prévia, o status do envio e possíveis erros. Para isso, usaremos hooks como <code>useState</code> e <code>useRef</code>, além de técnicas nativas de JavaScript para manipulação de files.</p>

<p>Vamos construir um componente robusto do zero, entendendo cada decisão arquitetural. Começaremos com a validação de tipo, depois adicionaremos preview e, por fim, o indicador de progresso. Cada camada será explicada antes do código, não o inverso.</p>

<h2>Validação de Tipo de Arquivo</h2>

<h3>Por que validar tipos?</h3>

<p>A validação ocorre em duas camadas: cliente e servidor. A validação no cliente é uma questão de experiência do usuário — previne que alguém tente enviar um PDF quando o formulário aceita apenas imagens. A validação no servidor é questão de segurança — sempre desconfie do cliente. Neste artigo, focamos na validação cliente, mas nunca esqueça: o servidor deve validar novamente.</p>

<p>Em React, acessamos o tipo MIME do arquivo através da propriedade <code>type</code> do objeto <code>File</code>. Contudo, essa propriedade pode ser enganada: um arquivo <code>.exe</code> renomeado como <code>.jpg</code> terá <code>type</code> vazio ou falso. A forma mais confiável é usar a extensão do arquivo em conjunto com a validação de tamanho. Para máxima segurança, o servidor deve re-validar usando bibliotecas como <code>file-type</code> (Node.js), que analisam bytes mágicos do arquivo.</p>

<pre><code class="language-javascript">// Hook customizado para validação

const useFileValidation = () =&gt; {

const [errors, setErrors] = useState([]);

const validateFile = (file, acceptedMimes, maxSizeMB = 5) =&gt; {

const validationErrors = [];

const maxSizeBytes = maxSizeMB 1024 1024;

// Validar tamanho

if (file.size &gt; maxSizeBytes) {

validationErrors.push(

Arquivo excede ${maxSizeMB}MB. Tamanho atual: ${(file.size / 1024 / 1024).toFixed(2)}MB

);

}

// Validar MIME type

if (!acceptedMimes.includes(file.type)) {

validationErrors.push(

Tipo de arquivo não permitido. Aceitos: ${acceptedMimes.join(&#039;, &#039;)}

);

}

// Validar extensão (camada adicional)

const extension = file.name.split(&#039;.&#039;).pop()?.toLowerCase();

const validExtensions = acceptedMimes

.map(mime =&gt; mime.split(&#039;/&#039;)[1])

.filter(Boolean);

if (!validExtensions.includes(extension)) {

validationErrors.push(

Extensão &quot;.${extension}&quot; não permitida

);

}

setErrors(validationErrors);

return validationErrors.length === 0;

};

return { errors, validateFile };

};</code></pre>

<p>Neste hook, implementamos três camadas de validação: tamanho do arquivo, tipo MIME declarado e extensão. A validação de extensão é redundante com o MIME, mas oferece proteção extra contra modificações simples no tipo MIME. Cada erro é armazenado em um array para que possamos exibir múltiplas mensagens ao usuário simultaneamente.</p>

<h2>Preview de Imagem e Arquivos</h2>

<h3>Gerando URLs de visualização</h3>

<p>Um preview eficiente em React requer compreender a API <code>FileReader</code> nativa do JavaScript. Quando o usuário seleciona uma imagem, precisamos convertê-la em uma URL que o navegador possa renderizar. O método mais moderno e eficiente é usar <code>URL.createObjectURL()</code>, que cria uma referência para o blob sem necessidade de leitura completa do arquivo. Para outros tipos de arquivo (PDF, documentos), podemos exibir ícones ou informações de metadados.</p>

<pre><code class="language-javascript">const FilePreview = ({ file, onClear }) =&gt; {

const [preview, setPreview] = useState(null);

const [fileInfo, setFileInfo] = useState(null);

useEffect(() =&gt; {

if (!file) return;

// Criar preview para imagens

if (file.type.startsWith(&#039;image/&#039;)) {

const objectUrl = URL.createObjectURL(file);

setPreview(objectUrl);

// Cleanup

return () =&gt; URL.revokeObjectURL(objectUrl);

} else {

// Para outros tipos, exibir metadados

setFileInfo({

name: file.name,

size: (file.size / 1024).toFixed(2),

type: file.type || &#039;Tipo desconhecido&#039;,

});

setPreview(null);

}

}, [file]);

if (!file) return null;

return (

&lt;div className=&quot;file-preview&quot;&gt;

{preview ? (

&lt;div className=&quot;image-preview&quot;&gt;

&lt;img src={preview} alt=&quot;Preview&quot; style={{ maxWidth: &#039;100%&#039;, maxHeight: &#039;300px&#039; }} /&gt;

&lt;/div&gt;

) : (

&lt;div className=&quot;file-info&quot;&gt;

&lt;p&gt;&lt;strong&gt;Arquivo:&lt;/strong&gt; {fileInfo?.name}&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tamanho:&lt;/strong&gt; {fileInfo?.size} KB&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tipo:&lt;/strong&gt; {fileInfo?.type}&lt;/p&gt;

&lt;/div&gt;

)}

&lt;button onClick={onClear} className=&quot;clear-button&quot;&gt;

Remover Arquivo

&lt;/button&gt;

&lt;/div&gt;

);

};</code></pre>

<p>Observe o detalhe crítico: <code>URL.revokeObjectURL()</code> é chamado no cleanup do <code>useEffect</code>. Sem isso, o objeto URL permanece na memória, causando vazamentos. Isso é especialmente importante em aplicações que permitem múltiplos uploads. Para imagens, usamos a preview visual; para outros tipos, exibimos metadados que ajudam o usuário a confirmar se selecionou o arquivo correto.</p>

<h2>Indicador de Progresso e Status do Upload</h2>

<h3>Implementando progresso com Fetch API</h3>

<p>O progresso de upload é frequentemente negligenciado porque a Fetch API não oferece suporte nativo. Precisamos usar <code>XMLHttpRequest</code> ou bibliotecas como <code>axios</code>, que encapsulam essa funcionalidade. Vou mostrar ambas as abordagens, começando com <code>XMLHttpRequest</code> (mais baixo nível, maior controle) e depois <code>axios</code> (mais simples e moderna).</p>

<p>A chave é monitorar o evento <code>upload.progress</code>, que dispara múltiplas vezes durante o envio, fornecendo bytes enviados e totais. Com isso, calculamos a porcentagem e atualizamos o estado React.</p>

<pre><code class="language-javascript">const useFileUpload = () =&gt; {

const [uploadProgress, setUploadProgress] = useState(0);

const [uploadStatus, setUploadStatus] = useState(&#039;idle&#039;); // idle, uploading, success, error

const [uploadError, setUploadError] = useState(null);

const uploadFileXHR = async (file, endpoint) =&gt; {

return new Promise((resolve, reject) =&gt; {

const xhr = new XMLHttpRequest();

const formData = new FormData();

formData.append(&#039;file&#039;, file);

// Monitor progresso

xhr.upload.addEventListener(&#039;progress&#039;, (event) =&gt; {

if (event.lengthComputable) {

const percentComplete = (event.loaded / event.total) * 100;

setUploadProgress(percentComplete);

}

});

// Sucesso

xhr.addEventListener(&#039;load&#039;, () =&gt; {

if (xhr.status &gt;= 200 &amp;&amp; xhr.status &lt; 300) {

setUploadStatus(&#039;success&#039;);

setUploadProgress(100);

resolve(JSON.parse(xhr.responseText));

} else {

setUploadStatus(&#039;error&#039;);

setUploadError(Erro do servidor: ${xhr.status});

reject(new Error(HTTP ${xhr.status}));

}

});

// Erro de rede

xhr.addEventListener(&#039;error&#039;, () =&gt; {

setUploadStatus(&#039;error&#039;);

setUploadError(&#039;Falha na conexão de rede&#039;);

reject(new Error(&#039;Network error&#039;));

});

// Cancelamento

xhr.addEventListener(&#039;abort&#039;, () =&gt; {

setUploadStatus(&#039;idle&#039;);

setUploadProgress(0);

reject(new Error(&#039;Upload cancelado&#039;));

});

setUploadStatus(&#039;uploading&#039;);

xhr.open(&#039;POST&#039;, endpoint);

xhr.send(formData);

});

};

const resetProgress = () =&gt; {

setUploadProgress(0);

setUploadStatus(&#039;idle&#039;);

setUploadError(null);

};

return {

uploadProgress,

uploadStatus,

uploadError,

uploadFileXHR,

resetProgress,

};

};</code></pre>

<p>O hook acima encapsula toda a lógica de upload. Observe que monitoramos eventos específicos: <code>progress</code> (durante envio), <code>load</code> (conclusão, sucesso ou erro), <code>error</code> (falhas de rede) e <code>abort</code> (cancelamento do usuário). Cada evento atualiza o estado React de forma apropriada. Essa abordagem oferece controle completo, mas é verbosa. Para projetos modernos, use <code>axios</code>, que abstrai esses detalhes:</p>

<pre><code class="language-javascript">import axios from &#039;axios&#039;;

const uploadFileAxios = async (file, endpoint) =&gt; {

try {

const formData = new FormData();

formData.append(&#039;file&#039;, file);

const response = await axios.post(endpoint, formData, {

onUploadProgress: (progressEvent) =&gt; {

const percentComplete = (progressEvent.loaded / progressEvent.total) * 100;

setUploadProgress(percentComplete);

},

headers: {

&#039;Content-Type&#039;: &#039;multipart/form-data&#039;,

},

});

setUploadStatus(&#039;success&#039;);

return response.data;

} catch (error) {

setUploadStatus(&#039;error&#039;);

setUploadError(error.message);

throw error;

}

};</code></pre>

<p>Com <code>axios</code>, o mesmo resultado é alcançado em metade do código. O callback <code>onUploadProgress</code> simplifica o monitoramento. A escolha entre <code>XMLHttpRequest</code> e <code>axios</code> depende das dependências do seu projeto: se já usa <code>axios</code>, não há motivo para <code>XMLHttpRequest</code>; se quer minimizar dependências externas, <code>XMLHttpRequest</code> nativo funciona perfeitamente.</p>

<h2>Componente Completo: Integração de Tudo</h2>

<h3>Juntando validação, preview e progresso</h3>

<p>Agora uniremos todos os conceitos em um componente robusto e pronto para produção. Este componente será reutilizável, com configurações via props.</p>

<pre><code class="language-javascript"></code></pre>

<p>Este componente integra toda a lógica: validação, preview, progresso e tratamento de erros. Note os detalhes importantes: o input é desabilitado durante upload para evitar seleções simultâneas; erros de validação impedem o upload; o progresso é arredondado para evitar atualizações excessivas; o cleanup (resetForm) prepara para novos uploads.</p>

<h2>Conclusão</h2>

<p>Os três pilares do upload robusto em React são: <strong>validação em camadas</strong> (tamanho, MIME, extensão), <strong>preview responsivo</strong> (usando <code>URL.createObjectURL</code> para imagens e metadados para outros tipos), e <strong>monitoramento de progresso</strong> (através de <code>onUploadProgress</code> do axios ou eventos <code>progress</code> do XMLHttpRequest). Implementar esses três aspectos simultaneamente evita frustrações do usuário e aumenta a confiabilidade da aplicação.</p>

<p>Um detalhe que muitos desenvolvedores esquecem é sempre validar novamente no servidor, pois toda validação cliente pode ser contornada. O React fornece a experiência visual; o backend fornece a segurança real.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/FormData" target="_blank" rel="noopener noreferrer">MDN Web Docs: FormData</a></li>

<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/File" target="_blank" rel="noopener noreferrer">MDN Web Docs: File API</a></li>

<li><a href="https://axios-http.com/docs/req_config" target="_blank" rel="noopener noreferrer">Axios Documentation: Request Config</a></li>

<li><a href="https://react.dev/reference/react" target="_blank" rel="noopener noreferrer">React Official Docs: Hooks</a></li>

<li><a href="https://xhr.spec.whatwg.org/" target="_blank" rel="noopener noreferrer">W3C XMLHttpRequest Standard</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em React & Frontend

Dominando Anatomia de Hooks Customizados: Composição e Separação de Concerns em Projetos Reais
Dominando Anatomia de Hooks Customizados: Composição e Separação de Concerns em Projetos Reais

O Que São Hooks Customizados e Por Que Importam Hooks customizados são funçõe...

useContext Avançado: Performance, Splitting e Evitando Re-renders na Prática
useContext Avançado: Performance, Splitting e Evitando Re-renders na Prática

O Problema Real do useContext em Aplicações Escaláveis Quando iniciamos com R...

Guia Completo de Arquiteturas de Estado em React: Local, Global, Server e URL State
Guia Completo de Arquiteturas de Estado em React: Local, Global, Server e URL State

Introdução: Os Quatro Pilares do Gerenciamento de Estado O gerenciamento de e...