npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-chunked-upload

v0.2.0

Published

A lightweight React hook for chunked large-file uploads with pause, resume, retry, and progress support.

Readme

react-chunked-upload

npm version npm downloads GitHub stars license

A lightweight React hook for chunked large-file uploads with pause, resume, cancel, automatic retries, timeouts, progress, dynamic headers, and multipart fields.

npm | GitHub | Report a bug | Request a feature

  • Small, headless React hook with no runtime dependencies
  • Sequential chunk uploads using the browser File and Blob APIs
  • Pause, resume, cancel, automatic retry with backoff, and byte-based progress
  • Static or per-request async headers for expiring credentials
  • Bring your own upload endpoint, authentication, storage, and UI

Installation

npm install react-chunked-upload

Examples

  • React demo - interactive Vite app with a built-in mock upload endpoint
  • Express server - receives, stores, and merges uploaded chunks

When to use it

Use react-chunked-upload when you need a small, UI-agnostic React file upload hook and control your own backend. It fits large video, archive, dataset, and document uploads where retrying a failed chunk is preferable to restarting the entire file.

For uploads that must resume after a browser refresh or across devices, use a persistent upload protocol such as tus instead. This package currently keeps upload state in memory for the active page session.

Why chunk files?

When uploading large files (e.g., 5GB videos or huge CSV datasets) in a traditional single-request manner, a network hiccup or timeout can cause the entire upload to fail, resulting in poor user experience and wasted bandwidth.

By splitting the file into small chunks (e.g., 5MB) on the client side using the HTML5 File and Blob APIs, react-chunked-upload provides:

  • Resiliency: A failed chunk can be retried without restarting completed chunks.
  • Control: You can pause and resume the upload at any time.
  • Feedback: Byte-based progress after each completed chunk.
  • Memory Efficiency: The browser doesn't need to load the entire file into memory at once.

Usage

import React, { useState } from 'react';
import { useChunkedUpload } from 'react-chunked-upload';

function App() {
  const [file, setFile] = useState<File | null>(null);
  
  const { 
    startUpload, 
    pauseUpload, 
    resumeUpload,
    retryUpload,
    cancelUpload,
    progress, 
    isUploading, 
    isPaused, 
    isError, 
    isSuccess 
  } = useChunkedUpload({
    chunkSize: 1024 * 1024 * 5, // 5MB chunks
    uploadUrl: 'https://your-api.com/upload-chunk',
    retries: 3, // auto-retry each chunk up to 3 times with backoff
    timeout: 30_000, // abort a chunk request that hangs for 30s
    // Static headers, or an async function evaluated before every chunk
    // request — handy when tokens can expire during a long upload.
    headers: async () => ({
      Authorization: `Bearer ${await getFreshToken()}`,
    }),
    fields: {
      folderId: 'invoices',
    },
    onChunkStart: (chunkIndex) => console.log(`Chunk ${chunkIndex} started`),
    onChunkSuccess: (chunkIndex) => console.log(`Chunk ${chunkIndex} uploaded`),
    onChunkError: (chunkIndex, err) => console.error(`Chunk ${chunkIndex} failed`, err),
    onSuccess: (response) => console.log('Upload complete!', response),
    onError: (err) => console.error('Upload failed', err),
    onProgress: (p) => console.log(`Progress: ${p}%`)
  });

  return (
    <div>
      <input type="file" onChange={e => setFile(e.target.files?.[0] || null)} />
      
      {!isUploading && !isPaused && (
        <button onClick={() => file && startUpload(file)}>Start</button>
      )}
      
      {isUploading && <button onClick={pauseUpload}>Pause</button>}
      {isPaused && <button onClick={resumeUpload}>Resume</button>}
      {isError && <button onClick={retryUpload}>Retry failed chunk</button>}
      {(isUploading || isPaused) && <button onClick={cancelUpload}>Cancel</button>}
      
      <div>Progress: {progress}%</div>
      {isSuccess && <div>Upload Successful! 🎉</div>}
      {isError && <div>Error uploading file.</div>}
    </div>
  );
}

API Reference

Options

| Option | Type | Default | Description | | --- | --- | --- | --- | | uploadUrl | string | Required | Endpoint that receives each chunk as multipart form data. | | chunkSize | number | 5 * 1024 * 1024 | Chunk size in bytes. Must be greater than 0. | | headers | HeadersInit \| () => HeadersInit \| Promise<HeadersInit> | undefined | Headers sent with every chunk request. A function is evaluated before every chunk attempt, so expiring tokens can be refreshed mid-upload. Do not set Content-Type manually when using FormData. | | fields | Record<string, string \| Blob> | undefined | Extra multipart fields appended to every chunk request. | | retries | number | 0 | Automatic retry attempts per chunk after the first failure. Only network errors, timeouts, HTTP 5xx, 408, and 429 are retried; other 4xx responses fail immediately. | | retryDelay | number \| (attempt: number, error: Error) => number | Exponential backoff | Delay in ms before retry attempt N (1-based). Defaults to 500ms doubling per attempt, capped at 10s. | | timeout | number | undefined | Per-attempt timeout in ms. A timed-out chunk request is aborted and counts as a retryable failure. | | onProgress | (progress: number) => void | undefined | Called after each completed chunk with byte-based progress from 0 to 100. | | onChunkStart | (chunkIndex: number) => void | undefined | Called before each chunk request attempt starts (once per retry attempt). | | onChunkSuccess | (chunkIndex: number, response: Response) => void | undefined | Called after each chunk request succeeds. Receives a cloned response. | | onChunkError | (chunkIndex: number, error: Error) => void | undefined | Called for each failed chunk request attempt, including attempts that will be retried. | | onSuccess | (response: Response) => void | undefined | Called after the final chunk succeeds. Receives the final HTTP Response. | | onError | (error: Error) => void | undefined | Called when validation fails or a chunk exhausts its retries. Chunk failures surface as ChunkUploadError with chunkIndex and status (when an HTTP response was received). |

Return value

| Value | Type | Description | | --- | --- | --- | | startUpload | (file: File) => void | Starts a new upload session for the selected file. | | pauseUpload | () => void | Aborts the in-flight request and pauses before the next chunk. | | resumeUpload | () => void | Continues from the last completed chunk. | | retryUpload | () => void | Retries from the failed chunk. Currently equivalent to resumeUpload. | | cancelUpload | () => void | Aborts the in-flight request, discards the session, and resets all state. A canceled upload cannot be resumed. | | progress | number | Upload progress from 0 to 100. | | isUploading | boolean | true while a chunk request is active. | | isPaused | boolean | true after pausing an active upload. | | isError | boolean | true after validation or request failure. | | isSuccess | boolean | true after the final chunk completes successfully. |

Backend Implementation

Your backend needs to handle the multipart form data sent by the hook. The hook sends one POST request per chunk to uploadUrl.

Multipart fields

| Field | Description | | --- | --- | | file | The binary chunk data. | | filename | The original file name. | | uploadId | A generated ID for the current upload attempt. Use this to isolate concurrent uploads. | | chunkIndex | The current chunk number, starting at 0. | | totalChunks | The total number of chunks for the file. |

Any fields values you provide are appended to the same multipart request, so your backend can receive project-specific metadata such as folderId or userId alongside each chunk.

The endpoint should store each chunk by uploadId and chunkIndex. When every chunk from 0 to totalChunks - 1 has been stored, merge/finalize the file before returning a successful response. Prefer checking that all chunks exist over reacting to chunkIndex === totalChunks - 1: the hook may re-send a chunk, so the final index can arrive more than once.

Idempotency requirement

Your endpoint must treat (uploadId, chunkIndex) as an idempotent key: receiving the same chunk twice must be safe and must not duplicate data. If pauseUpload() is called while a chunk response is in flight, the hook does not record that chunk as completed and re-sends it on resume. In that case onChunkSuccess also fires more than once for the same chunk index. Storing each chunk at a path derived from uploadId and chunkIndex (overwriting on re-receive), as the Express example does, satisfies this naturally.

Express example

See examples/express-server for a runnable Express server that stores chunks and merges the final file.

import express from 'express';
import multer from 'multer';
import { mkdir, readdir, rename } from 'node:fs/promises';
import path from 'node:path';

const app = express();
const upload = multer({ dest: 'tmp/chunks' });

app.post('/upload-chunk', upload.single('file'), async (req, res) => {
  const { filename, uploadId, chunkIndex, totalChunks } = req.body;

  if (!req.file || !filename || !uploadId || chunkIndex == null || !totalChunks) {
    return res.status(400).json({ message: 'Missing chunk upload fields' });
  }

  const uploadDir = path.join('tmp/uploads', uploadId);
  await mkdir(uploadDir, { recursive: true });

  // Overwriting on re-receive keeps (uploadId, chunkIndex) idempotent.
  const chunkPath = path.join(uploadDir, String(chunkIndex));
  await rename(req.file.path, chunkPath);

  const received = await readdir(uploadDir);
  const hasAllChunks = received.length === Number(totalChunks);

  if (hasAllChunks) {
    // Merge chunks 0..totalChunks - 1 into the final file here.
    // Only send a 2xx response after the merge/finalization succeeds.
  }

  return res.status(200).json({ uploadId, filename, chunkIndex, totalChunks });
});

Behavior Notes

  • Uploads are sequential: the next chunk starts after the previous chunk succeeds.
  • Progress is updated after each completed chunk, not continuously while a chunk is streaming.
  • Pausing aborts the active request and resumes from the last completed chunk. A chunk whose response was in flight when you paused is re-sent on resume, so servers must treat (uploadId, chunkIndex) idempotently (see Idempotency requirement above).
  • Automatic retries are opt-in (retries defaults to 0). Retryable failures are network errors, timeouts, HTTP 5xx, 408, and 429; other 4xx responses fail immediately. onChunkError fires for every failed attempt; onError fires once when a chunk gives up.
  • timeout aborts a hung chunk request and treats it as a retryable failure. Pause, cancel, and unmount aborts are never retried.
  • A headers function is evaluated before every chunk attempt; a static headers value is snapshotted when the upload starts.
  • Calling startUpload with invalid options reports an error but leaves a paused session resumable.
  • This package does not persist upload state across browser refreshes yet.
  • This package does not merge chunks on the server; your backend owns storage and finalization.
  • Custom headers are sent with every chunk request, but Content-Type should be left to the browser when using FormData.

Comparison

| Package | Best fit | Notes | | --- | --- | --- | | react-chunked-upload | A small React hook for sequential chunk uploads with pause, resume, retry, headers, and multipart fields. | Bring your own backend merge logic. | | @rpldy/chunked-uploady | A fuller upload framework with Uploady ecosystem integrations. | More features and package surface than a single hook. | | @rpldy/tus-sender | Apps that use the tus resumable upload protocol. | Requires a tus-compatible backend. | | rc-upload | General-purpose upload UI plumbing. | Not focused on chunk merge workflows by default. | | Uploadcare | Managed upload, storage, and CDN workflows. | External service instead of a small client-side library. |

Community

Using react-chunked-upload in a project? Open a Show and tell issue to share what you built. Real-world use cases help guide compatibility and future releases.

Bug reports and focused feature requests are welcome. See CONTRIBUTING.md before opening a pull request.

License

MIT