For modern web applications, the file upload experience is a critical interaction point. Whether users are submitting documents, uploading profile assets, or transferring large datasets, providing immediate, granular feedback via a progress bar is non-negotiable for a professional user experience. Without this visual indicator, users are left guessing whether a request is stalling or successfully transmitting, leading to increased abandonment rates.
In the React ecosystem, implementing this requires more than just a standard HTML file input. It demands a deep understanding of the XMLHttpRequest API—which remains the most reliable way to track upload progress—and how to bridge that low-level browser functionality with React’s state management. This guide provides a production-ready implementation, focusing on performance, state synchronization, and the trade-offs between different architectural approaches.
The Architectural Challenge of Upload Progress
The core challenge in tracking upload progress stems from the Fetch API’s limitations. While Fetch is the modern standard for network requests, it does not natively support onProgress callbacks for request bodies. To accurately capture the bytes-sent-to-bytes-total ratio, developers must revert to the XMLHttpRequest (XHR) object. This is a common point of friction for developers accustomed to clean async/await syntax.
When building this in React, you must wrap the XHR logic within a custom hook. This decouples the network transmission logic from your UI components, ensuring your codebase remains maintainable as the application scales. The key is correctly mapping the progress event emitted by the XHR upload property to a React state variable that drives your progress bar component.
Implementing the Custom Upload Hook
A custom hook approach allows you to encapsulate the complexity of event listeners and state updates. Below is a implementation that handles the file upload lifecycle, including progress tracking and error handling.
const useFileUpload = (url) => { const [progress, setProgress] = useState(0); const [error, setError] = useState(null); const uploadFile = (file) => { const xhr = new XMLHttpRequest(); const formData = new FormData(); formData.append('file', file); xhr.upload.onprogress = (event) => { if (event.lengthComputable) { setProgress(Math.round((event.loaded * 100) / event.total)); } }; xhr.onload = () => { if (xhr.status === 200) { setProgress(100); } else { setError('Upload failed'); } }; xhr.onerror = () => setError('Network error'); xhr.open('POST', url); xhr.send(formData); }; return { progress, error, uploadFile }; };
Optimizing UI Feedback with React State
Once the hook is in place, the UI component simply consumes the progress state. To prevent unnecessary re-renders, ensure that your progress bar component is memoized or kept minimal. If you are updating the progress state extremely rapidly (e.g., for very large files), consider debouncing the state update or using a ref if the UI doesn’t strictly require a re-render at every single byte change.
For complex dashboards, integrating this with React Query can be beneficial. While React Query handles GET requests exceptionally well, POST requests for file uploads are often better managed via direct XHR or Axios instances to maintain granular control over the progress event listeners.
Security and Performance Considerations
Security is paramount when handling user-provided files. Never rely on client-side validation alone. Always implement server-side checks for file type (MIME sniffing), file size, and malware scanning. On the client side, use the accept attribute on your <input type="file" /> to guide user selection, but treat this as a UX feature, not a security boundary.
Performance-wise, for large files, consider chunked uploads. Instead of one monolithic request, split the file into smaller blobs on the client side and upload them sequentially. This allows for easier retry logic if a single chunk fails, significantly improving reliability for unstable network conditions.
Trade-offs: XHR vs. Libraries
You face a fundamental choice: building a custom XHR implementation or utilizing a library like Axios or Uppy. A custom XHR implementation provides the smallest bundle size and zero external dependencies, which is ideal for performance-sensitive applications. However, it requires more boilerplate and manual error handling.
Libraries like Axios offer a cleaner API and built-in progress event support, reducing the amount of code you write. The trade-off is an increased bundle size. For most SaaS applications, the development velocity gained by using a library outweighs the minor performance cost of the added dependency.
Decision Framework for Implementation
How do you choose the right path for your project? Use this framework:
- Choose Custom XHR: If you are building a lightweight, highly performance-optimized dashboard or a PWA where every kilobyte of bundle size matters.
- Choose Axios: If you already have it as a dependency in your project and need to implement uploads quickly without writing custom wrappers.
- Choose Uppy/FilePond: If you need complex features like image resizing, multiple file selection, drag-and-drop, and S3 direct uploads out of the box.
Factors That Affect Development Cost
- Complexity of file handling requirements (e.g., chunking, resumable uploads)
- Integration with cloud storage providers like AWS S3
- Need for client-side image processing or validation
- Development time for custom vs library-based solutions
Costs are largely driven by the time required to implement robust error handling and backend coordination rather than the UI component itself.
Frequently Asked Questions
How to show file upload progress bar in React?
To show a progress bar in React, you must use the XMLHttpRequest object rather than the standard Fetch API. By attaching an event listener to the xhr.upload.onprogress event, you can track the upload percentage and store it in React state to update your UI.
How to make a file upload progress bar?
You create a progress bar by linking a state variable to the width of an HTML element or an SVG stroke-dashoffset. As the upload progresses, you update the state value, causing the component to re-render and visually represent the completion percentage.
How do I upload a file with React?
You use an HTML input element with the type set to file. When the user selects a file, you capture the File object from the event target, append it to a FormData object, and send it to your server via an HTTP request.
Implementing a file upload progress bar is a direct reflection of your application’s attention to detail. By bridging raw browser APIs with React’s declarative state, you create a responsive and professional experience that keeps users informed. Whether you opt for a lean, custom-built solution or a robust library, the goal remains the same: stability and transparency.
If your team is struggling with complex file handling or needs help scaling your frontend architecture, NR Studio is here to assist. We specialize in building high-performance, maintainable React and Next.js applications tailored to your specific business requirements. Let’s discuss how we can improve your user experience.
NR Studio builds custom web apps, mobile apps, SaaS platforms, and internal tools for growing businesses. If you’re working through a technical decision, feel free to reach out — no commitment required.