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.
