Illustration for React Integration example

React Integration

Using Resumable.js with React hooks and component patterns for file uploads.

Examples·Updated 2025-11-05

React Integration

Resumable.js was written before hooks existed, yet it maps to React's lifecycle model surprisingly well — a ref holds the instance, an effect handles setup and teardown, and state drives the render. This page walks through a production-style <FileUploader /> component covering initialization, progress tracking, completion, and cleanup. More integration patterns live in the Examples hub.

The Core Problem

Resumable.js manages its own internal state: file queues, chunk progress, retry counters. React also wants to own rendering state. The trick is keeping them in sync without fighting each other. A useRef prevents re-instantiation on every render, while useState variables mirror the values React needs to display.

Full Component

import { useRef, useState, useEffect, useCallback } from 'react';
import Resumable from 'resumablejs';

interface UploadFile {
  uniqueIdentifier: string;
  fileName: string;
  progress: number;
  status: 'uploading' | 'complete' | 'error';
}

export default function FileUploader({ target = '/api/upload' }) {
  const resumableRef = useRef<Resumable | null>(null);
  const browseRef = useRef<HTMLButtonElement>(null);
  const dropRef = useRef<HTMLDivElement>(null);

  const [files, setFiles] = useState<UploadFile[]>([]);
  const [overallProgress, setOverallProgress] = useState(0);
  const [uploading, setUploading] = useState(false);

  useEffect(() => {
    const r = new Resumable({
      target,
      chunkSize: 2 * 1024 * 1024,
      simultaneousUploads: 3,
      testChunks: true,
      throttleProgressCallbacks: 0.3,
    });

    resumableRef.current = r;

    if (browseRef.current) r.assignBrowse(browseRef.current);
    if (dropRef.current) r.assignDrop(dropRef.current);

    r.on('fileAdded', (file: any) => {
      setFiles((prev) => [
        ...prev,
        {
          uniqueIdentifier: file.uniqueIdentifier,
          fileName: file.fileName,
          progress: 0,
          status: 'uploading',
        },
      ]);
      r.upload();
      setUploading(true);
    });

    r.on('fileProgress', (file: any) => {
      const pct = Math.round(file.progress() * 100);
      setFiles((prev) =>
        prev.map((f) =>
          f.uniqueIdentifier === file.uniqueIdentifier
            ? { ...f, progress: pct }
            : f
        )
      );
      setOverallProgress(Math.round(r.progress() * 100));
    });

    r.on('fileSuccess', (file: any) => {
      setFiles((prev) =>
        prev.map((f) =>
          f.uniqueIdentifier === file.uniqueIdentifier
            ? { ...f, progress: 100, status: 'complete' }
            : f
        )
      );
    });

    r.on('fileError', (file: any, message: string) => {
      setFiles((prev) =>
        prev.map((f) =>
          f.uniqueIdentifier === file.uniqueIdentifier
            ? { ...f, status: 'error' }
            : f
        )
      );
      console.error(`Upload error for ${file.fileName}:`, message);
    });

    r.on('complete', () => {
      setUploading(false);
    });

    // Cleanup on unmount
    return () => {
      r.cancel();
      resumableRef.current = null;
    };
  }, [target]);

  const handlePause = useCallback(() => {
    resumableRef.current?.pause();
    setUploading(false);
  }, []);

  const handleResume = useCallback(() => {
    resumableRef.current?.upload();
    setUploading(true);
  }, []);

  return (
    <div className="uploader">
      <div
        ref={dropRef}
        className="dropzone"
        style={{
          border: '2px dashed #94a3b8',
          borderRadius: '8px',
          padding: '2rem',
          textAlign: 'center',
          marginBottom: '1rem',
        }}
      >
        Drop files here or{' '}
        <button ref={browseRef} className="browse-btn">
          browse
        </button>
      </div>

      {files.length > 0 && (
        <>
          <div className="overall-progress" style={{ marginBottom: '1rem' }}>
            <strong>Overall: {overallProgress}%</strong>
            <div style={{ background: '#e5e7eb', borderRadius: 4, height: 8 }}>
              <div
                style={{
                  width: `${overallProgress}%`,
                  background: '#2563eb',
                  height: '100%',
                  borderRadius: 4,
                  transition: 'width 0.2s',
                }}
              />
            </div>
          </div>

          <ul style={{ listStyle: 'none', padding: 0 }}>
            {files.map((f) => (
              <li key={f.uniqueIdentifier} style={{ marginBottom: '0.5rem' }}>
                <span>{f.fileName}</span>{' '}
                <span>
                  {f.status === 'complete'
                    ? '✓'
                    : f.status === 'error'
                    ? '✗'
                    : `${f.progress}%`}
                </span>
              </li>
            ))}
          </ul>

          <div style={{ display: 'flex', gap: '0.5rem' }}>
            {uploading ? (
              <button onClick={handlePause}>Pause</button>
            ) : (
              <button onClick={handleResume}>Resume</button>
            )}
          </div>
        </>
      )}
    </div>
  );
}

Key Decisions Explained

Why useRef instead of useState for the Resumable instance? Because the instance is mutable, long-lived, and doesn't need to trigger re-renders on its own. Storing it in state would cause unnecessary render cycles every time an internal property changed.

Why useCallback for pause/resume? These handlers reference resumableRef.current, which is stable across renders. Wrapping them avoids creating new function identities, which matters if you pass them to memoized child components.

Cleanup is non-negotiable. Calling r.cancel() in the effect's return function aborts in-flight XHRs. Without it, navigating away mid-upload leaves orphaned network requests and potential memory leaks — exactly the kind of bug that only surfaces in production.

Usage

Drop the component anywhere in your app:

export default function UploadPage() {
  return (
    <main>
      <h1>Upload Files</h1>
      <FileUploader target="/api/upload" />
    </main>
  );
}

The target prop lets you point different instances at different endpoints — one for avatars, another for documents — without duplicating component code.

Adapting for Next.js or Remix

The same component works in Next.js App Router with one addition: mark it as a client component with 'use client' at the top, since Resumable.js accesses browser APIs (File, Blob, XMLHttpRequest). In Remix, import it lazily inside a ClientOnly boundary so the server render doesn't choke on missing globals.

No framework magic needed. Resumable.js is framework-agnostic plumbing — React just provides the rendering layer on top.