What is your developer-dependent CMS actually costing you? Calculate your costs and get a full report

Agility CMS

How to Handle File Uploads in React: Buffering, Progress & Preview

Tips on building a smooth, client-side file upload component

Agility CMS

Agility CMS

Canada's Original Headless CMS

Developer ToolsHow To Developer ToolsDeveloper
Graphic of files being uploaded to the Cloud

Uploading large files in a React application can be tricky: you don’t want to freeze the UI, lose users when uploads hang, or overwhelm your backend with massive payloads.

In this tutorial, we’ll build a smooth, client-side file upload component that:

  • Reads files in buffered chunks.

  • Displays a live progress bar.

  • Shows a preview (for images) before uploading.

  • Sends each chunk to your API endpoint.


Table of Contents

  1. Why Buffering Matters 
  2. Setting Up the File Input & State

  3. Reading Files in Chunks

  4. Displaying a Progress Bar

  5. Rendering an Image Preview

  6. Uploading Chunks to Your Server

  7. Putting It All Together

  8. Further Reading & Internal Links


1. Why Buffering Matters

When you attempt to upload a multi-megabyte file all at once:

  • UI freezes while the browser processes the data.

  • Network failures force you to restart from zero.

  • Memory usage spikes, affecting performance.

By slicing the file into smaller chunks (e.g. 512 KB each), you can:

  • Stream data progressively with the Fetch API.

  • Update a progress indicator after each chunk.

  • Retry individual chunks on failure.


2. Setting Up the File Input & State

First, create a React component with state to hold the selected file, upload progress, and preview URL.

jsx

CopyEdit

import React, { useState } from 'react';

export default function FileUploader() {
  const [file, setFile] = useState(null);
  const [previewURL, setPreviewURL] = useState('');
  const [progress, setProgress] = useState(0);

  function handleFileChange(e) {
    const selected = e.target.files[0];
    if (selected) {
      setFile(selected);
      setPreviewURL(URL.createObjectURL(selected));
    }
  }

  return (
    <div>
      <input type="file" onChange={handleFileChange} />
      {previewURL && (
        <img src={previewURL} alt="Preview" style={{ maxWidth: 200 }} />
      )}
      {file && <button onClick={() => uploadInChunks(file)}>Upload</button>}
      {progress > 0 && <progress value={progress} max="100" />}
    </div>
  );
}


3. Reading Files in Chunks

Use the File API’s slice method to break the file into buffers:

js

CopyEdit

async function* sliceFile(file, chunkSize = 512 * 1024) {
  let offset = 0;
  while (offset < file.size) {
    const chunk = file.slice(offset, offset + chunkSize);
    yield chunk;
    offset += chunkSize;
  }
}

This async generator yields Blob chunks you can upload sequentially.


4. Displaying a Progress Bar

Inside your upload loop, update progress after each chunk:

js

CopyEdit

async function uploadInChunks(file) {
  const totalChunks = Math.ceil(file.size / chunkSize);
  let uploaded = 0;

  for await (const chunk of sliceFile(file, chunkSize)) {
    await uploadChunk(chunk, file.name, uploaded);
    uploaded++;
    setProgress(Math.round((uploaded / totalChunks) * 100));
  }
}

With setProgress, React re-renders the <progress> element, giving live feedback.


5. Rendering an Image Preview

We already generate a preview URL via URL.createObjectURL. To clean up memory:

js

CopyEdit

useEffect(() => {
  return () => {
    if (previewURL) URL.revokeObjectURL(previewURL);
  };
}, [previewURL]);

This releases the blob URL when the component unmounts or a new file is selected.


6. Uploading Chunks to Your Server

Assuming your server expects:

yaml

CopyEdit

POST /api/upload
Headers:
  Content-Range: bytes {start}-{end}/{total}
Body: raw chunk bytes

Implement uploadChunk like so:

js

CopyEdit

async function uploadChunk(chunk, fileName, chunkIndex) {
  const start = chunkIndex * chunkSize;
  const end = start + chunk.size - 1;

  const res = await fetch('/api/upload', {
    method: 'POST',
    headers: {
      'Content-Range': `bytes ${start}-${end}/${file.size}`,
      'X-File-Name': fileName,
    },
    body: chunk,
  });

  if (!res.ok) {
    throw new Error(`Chunk upload failed: ${res.statusText}`);
  }
}

On the backend, you can assemble the chunks in order. see our guide on API vs. Webhooks: What’s the Difference? for webhook-based ingestion patterns.


7. Putting It All Together

Here’s the full uploader component with everything wired:

jsx

CopyEdit

import React, { useState, useEffect } from 'react';

const chunkSize = 512 * 1024; // 512 KB

async function* sliceFile(file) { /*…*/ }
async function uploadChunk(chunk, fileName, chunkIndex) { /*…*/ }

export default function FileUploader() {
  const [file, setFile] = useState(null);
  const [previewURL, setPreviewURL] = useState('');
  const [progress, setProgress] = useState(0);

  useEffect(() => () => previewURL && URL.revokeObjectURL(previewURL), [previewURL]);

  async function handleUpload() {
    const totalChunks = Math.ceil(file.size / chunkSize);
    let uploaded = 0;
    for await (const chunk of sliceFile(file)) {
      await uploadChunk(chunk, file.name, uploaded);
      uploaded++;
      setProgress(Math.round((uploaded / totalChunks) * 100));
    }
  }

  return (
    <div>
      <input type="file" onChange={e => {
        const f = e.target.files[0];
        setFile(f);
        setPreviewURL(URL.createObjectURL(f));
        setProgress(0);
      }} />

      {previewURL && <img src={previewURL} alt="Preview" style={{ maxWidth: 200 }} />}

      {file && <button onClick={handleUpload}>Upload</button>}

      {progress > 0 && <progress value={progress} max="100">{progress}%</progress>}
    </div>
  );
}


With buffering, progress indicators, and previews in place, your React app can handle large file uploads without sacrificing performance or user experience.