<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 = () => {
const [errors, setErrors] = useState([]);
const validateFile = (file, acceptedMimes, maxSizeMB = 5) => {
const validationErrors = [];
const maxSizeBytes = maxSizeMB 1024 1024;
// Validar tamanho
if (file.size > 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(', ')}
);
}
// Validar extensão (camada adicional)
const extension = file.name.split('.').pop()?.toLowerCase();
const validExtensions = acceptedMimes
.map(mime => mime.split('/')[1])
.filter(Boolean);
if (!validExtensions.includes(extension)) {
validationErrors.push(
Extensão ".${extension}" 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 }) => {
const [preview, setPreview] = useState(null);
const [fileInfo, setFileInfo] = useState(null);
useEffect(() => {
if (!file) return;
// Criar preview para imagens
if (file.type.startsWith('image/')) {
const objectUrl = URL.createObjectURL(file);
setPreview(objectUrl);
// Cleanup
return () => URL.revokeObjectURL(objectUrl);
} else {
// Para outros tipos, exibir metadados
setFileInfo({
name: file.name,
size: (file.size / 1024).toFixed(2),
type: file.type || 'Tipo desconhecido',
});
setPreview(null);
}
}, [file]);
if (!file) return null;
return (
<div className="file-preview">
{preview ? (
<div className="image-preview">
<img src={preview} alt="Preview" style={{ maxWidth: '100%', maxHeight: '300px' }} />
</div>
) : (
<div className="file-info">
<p><strong>Arquivo:</strong> {fileInfo?.name}</p>
<p><strong>Tamanho:</strong> {fileInfo?.size} KB</p>
<p><strong>Tipo:</strong> {fileInfo?.type}</p>
</div>
)}
<button onClick={onClear} className="clear-button">
Remover Arquivo
</button>
</div>
);
};</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 = () => {
const [uploadProgress, setUploadProgress] = useState(0);
const [uploadStatus, setUploadStatus] = useState('idle'); // idle, uploading, success, error
const [uploadError, setUploadError] = useState(null);
const uploadFileXHR = async (file, endpoint) => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append('file', file);
// Monitor progresso
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
setUploadProgress(percentComplete);
}
});
// Sucesso
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
setUploadStatus('success');
setUploadProgress(100);
resolve(JSON.parse(xhr.responseText));
} else {
setUploadStatus('error');
setUploadError(Erro do servidor: ${xhr.status});
reject(new Error(HTTP ${xhr.status}));
}
});
// Erro de rede
xhr.addEventListener('error', () => {
setUploadStatus('error');
setUploadError('Falha na conexão de rede');
reject(new Error('Network error'));
});
// Cancelamento
xhr.addEventListener('abort', () => {
setUploadStatus('idle');
setUploadProgress(0);
reject(new Error('Upload cancelado'));
});
setUploadStatus('uploading');
xhr.open('POST', endpoint);
xhr.send(formData);
});
};
const resetProgress = () => {
setUploadProgress(0);
setUploadStatus('idle');
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 'axios';
const uploadFileAxios = async (file, endpoint) => {
try {
const formData = new FormData();
formData.append('file', file);
const response = await axios.post(endpoint, formData, {
onUploadProgress: (progressEvent) => {
const percentComplete = (progressEvent.loaded / progressEvent.total) * 100;
setUploadProgress(percentComplete);
},
headers: {
'Content-Type': 'multipart/form-data',
},
});
setUploadStatus('success');
return response.data;
} catch (error) {
setUploadStatus('error');
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><!-- FIM --></p>