Resumable.js is a browser-only library. It touches the DOM, reads from the File API, and creates XMLHttpRequest objects — none of which exist on a server. Modern frameworks like Next.js, Nuxt, SvelteKit, and even plain React with SSR render components on the server first, then hydrate on the client. If you import and initialize Resumable.js without accounting for this, you'll get a ReferenceError: window is not defined and a broken build.
This guide covers the practical patterns for integrating Resumable.js into React, Next.js, and Svelte — the frameworks where SSR initialization is the primary pitfall. The same principles apply to Vue, Angular, and any other framework with server-side rendering.
The core rule
Never initialize Resumable.js outside a browser context. Every framework has its own mechanism for saying "run this code only on the client" — useEffect in React, onMount in Svelte, mounted in Vue. Use it.
React: custom hook with full lifecycle
The cleanest React pattern wraps Resumable.js in a custom hook that handles initialization, event binding, and cleanup.
import { useEffect, useRef, useState, useCallback } from 'react';
import Resumable from 'resumablejs';
interface UseResumableOptions {
target: string;
chunkSize?: number;
simultaneousUploads?: number;
testChunks?: boolean;
}
export function useResumable(options: UseResumableOptions) {
const resumableRef = useRef<Resumable | null>(null);
const [progress, setProgress] = useState(0);
const [isUploading, setIsUploading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const r = new Resumable({
target: options.target,
chunkSize: options.chunkSize ?? 2 * 1024 * 1024,
simultaneousUploads: options.simultaneousUploads ?? 3,
testChunks: options.testChunks ?? true,
});
r.on('fileAdded', () => {
setError(null);
});
r.on('uploadStart', () => {
setIsUploading(true);
});
r.on('fileProgress', (file) => {
setProgress(Math.floor(file.progress() * 100));
});
r.on('fileSuccess', () => {
setIsUploading(false);
setProgress(100);
});
r.on('fileError', (_file, message) => {
setIsUploading(false);
setError(message);
});
resumableRef.current = r;
return () => {
r.cancel();
resumableRef.current = null;
};
}, [options.target, options.chunkSize,
options.simultaneousUploads, options.testChunks]);
const assignBrowse = useCallback((element: HTMLElement) => {
resumableRef.current?.assignBrowse(element);
}, []);
const assignDrop = useCallback((element: HTMLElement) => {
resumableRef.current?.assignDrop(element);
}, []);
const upload = useCallback(() => {
resumableRef.current?.upload();
}, []);
return { progress, isUploading, error, assignBrowse, assignDrop, upload };
}
Use it in a component:
function UploadButton() {
const browseRef = useRef<HTMLButtonElement>(null);
const { progress, isUploading, assignBrowse, upload } = useResumable({
target: '/api/upload',
});
useEffect(() => {
if (browseRef.current) {
assignBrowse(browseRef.current);
}
}, [assignBrowse]);
return (
<div>
<button ref={browseRef}>Select Files</button>
<button onClick={upload} disabled={isUploading}>Upload</button>
{isUploading && <p>Progress: {progress}%</p>}
</div>
);
}
The key details: Resumable.js is instantiated inside useEffect, which only runs in the browser. The ref keeps the instance stable across renders. Cleanup calls r.cancel() to abort any in-flight uploads and release event listeners.
For a more complete React integration, see the React integration example.
Next.js: dynamic import or lazy initialization
Next.js renders pages on the server by default (both Pages Router and App Router). You have two options for keeping Resumable.js client-only.
Option A: dynamic import with ssr: false (Pages Router)
import dynamic from 'next/dynamic';
const UploadWidget = dynamic(() => import('../components/UploadWidget'), {
ssr: false,
loading: () => <p>Loading uploader...</p>,
});
export default function UploadPage() {
return (
<main>
<h1>Upload Files</h1>
<UploadWidget />
</main>
);
}
The UploadWidget component imports and uses Resumable.js normally — it will never be rendered on the server.
Option B: lazy initialization in useEffect (App Router)
In the App Router, you can mark a component with 'use client' and dynamically import Resumable.js inside useEffect:
'use client';
import { useEffect, useRef, useState } from 'react';
export default function UploadWidget() {
const [Resumable, setResumable] = useState<any>(null);
const instanceRef = useRef<any>(null);
const browseRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
import('resumablejs').then((mod) => {
const ResumableClass = mod.default || mod;
setResumable(() => ResumableClass);
});
}, []);
useEffect(() => {
if (!Resumable || !browseRef.current) return;
const r = new Resumable({
target: '/api/upload',
chunkSize: 2 * 1024 * 1024,
});
r.assignBrowse(browseRef.current);
r.on('fileAdded', () => r.upload());
instanceRef.current = r;
return () => {
r.cancel();
instanceRef.current = null;
};
}, [Resumable]);
return <button ref={browseRef}>Select Files</button>;
}
This pattern dynamically imports the Resumable.js module at runtime, guaranteeing it never executes during SSR. The two-stage useEffect ensures the class is loaded before instantiation.
Both approaches work. Option A is cleaner when the entire component is upload-specific. Option B is more flexible when the upload widget is part of a larger client component.
Svelte: onMount and onDestroy
SvelteKit renders components on the server. Svelte's onMount lifecycle hook runs only in the browser, making it the natural place for Resumable.js initialization.
<script>
import { onMount, onDestroy } from 'svelte';
let resumable;
let progress = 0;
let isUploading = false;
let browseButton;
let dropZone;
onMount(async () => {
const { default: Resumable } = await import('resumablejs');
resumable = new Resumable({
target: '/api/upload',
chunkSize: 2 * 1024 * 1024,
simultaneousUploads: 3,
testChunks: true,
});
resumable.assignBrowse(browseButton);
resumable.assignDrop(dropZone);
resumable.on('fileAdded', () => {
resumable.upload();
});
resumable.on('fileProgress', (file) => {
progress = Math.floor(file.progress() * 100);
isUploading = true;
});
resumable.on('fileSuccess', () => {
isUploading = false;
progress = 100;
});
resumable.on('fileError', (_file, message) => {
isUploading = false;
console.error('Upload error:', message);
});
});
onDestroy(() => {
if (resumable) {
resumable.cancel();
resumable = null;
}
});
</script>
<div bind:this={dropZone} class="drop-zone">
<button bind:this={browseButton}>Select Files</button>
{#if isUploading}
<p>Uploading: {progress}%</p>
{/if}
</div>
Svelte's reactivity makes this particularly clean — progress and isUploading are reactive variables that update the DOM automatically when Resumable.js events fire. The bind:this directive gives direct access to DOM elements for assignBrowse and assignDrop without ref boilerplate.
Dynamic import() inside onMount ensures the Resumable.js module is only loaded in the browser. This is critical in SvelteKit, where top-level imports are evaluated during SSR.
Common pitfalls across all frameworks
1. Initializing during SSR
The most frequent mistake. Any import or instantiation of Resumable.js that runs on the server will crash with ReferenceError: window is not defined or ReferenceError: document is not defined. Always gate initialization behind a client-only lifecycle hook.
2. Not cleaning up on unmount
If a component unmounts while an upload is in progress (user navigates away, modal closes), the Resumable.js instance keeps running. Orphaned instances leak memory and fire events into unmounted components, causing React "Can't perform a state update on an unmounted component" warnings. Always call r.cancel() in your cleanup function.
3. Stale closures in event callbacks
In React, event callbacks registered in useEffect capture the state values from that render. If you reference state variables inside a Resumable.js event handler, they'll be stale on subsequent renders. Use refs for values that need to be current inside callbacks:
const filesRef = useRef<ResumableFile[]>([]);
r.on('fileAdded', (file) => {
// Don't use `files` state here — it's stale
filesRef.current = [...filesRef.current, file];
setFiles([...filesRef.current]);
});
4. Hydration mismatches
If your server-rendered HTML shows a "Loading..." placeholder but the client renders an upload button, React will warn about hydration mismatches. Either render the same placeholder on both sides (using dynamic with ssr: false in Next.js) or suppress the mismatch with a client-only state flag.
5. Re-initializing on every render
If your useEffect dependency array isn't stable, Resumable.js gets destroyed and recreated on every render. This cancels in-progress uploads. Memoize your configuration options or use a ref to track whether initialization has already happened.
Framework-agnostic tips
Debounce progress updates. Resumable.js fires fileProgress on every chunk completion. If you're uploading many small chunks, that's a lot of re-renders. Debounce or throttle progress state updates to ~200ms intervals:
let lastUpdate = 0;
r.on('fileProgress', (file) => {
const now = Date.now();
if (now - lastUpdate > 200) {
setProgress(Math.floor(file.progress() * 100));
lastUpdate = now;
}
});
Keep the Resumable instance outside component state. Store it in a useRef (React), a plain variable (Svelte), or a non-reactive property (Vue). Putting it in state triggers unnecessary re-renders and can cause infinite update loops.
Handle the events you need, ignore the rest. Resumable.js has a rich event system. You don't need to bind every event — start with fileAdded, fileProgress, fileSuccess, and fileError. Add more events as your UI requirements grow.
The patterns here should translate to any framework with an SSR story. The principle is always the same: initialize in the browser, clean up on unmount, and keep the instance stable.
