Uploads and frontend integration

# React file uploads with Uppy: resumability, previews, validation, and processing

Build a React file uploader with Uppy that handles previews, validation, accessible progress, cancellation, resumable transfer, and a Transloadit processing handoff.

Published August 24, 2026

## Key takeaways

* Create one Uppy instance per mounted uploader and destroy it when the owning component unmounts.
* Use Uppy state and events as external state instead of copying progress, files, and errors into competing React state.
* Treat client restrictions as immediate feedback and enforce the same policy again in a trusted Template or receiver.

A React file upload is a small stateful system, not just an input and a POST request. The uploader must survive rerenders, release resources on unmount, explain restrictions before transfer, recover from ordinary network failures, and distinguish uploaded bytes from finished media processing. Uppy supplies that upload state machine while React renders the current state.

## In this guide

1. [Define what “complete” means before writing the component](#react-file-uploads-with-uppy-section-1)
2. [Create one configured Uppy instance per mounted uploader](#react-file-uploads-with-uppy-section-2)
3. [Render progress, cancellation, and Assembly state from Uppy](#react-file-uploads-with-uppy-section-3)
4. [Use previews and restrictions as interface features, not trust boundaries](#react-file-uploads-with-uppy-section-4)
5. [Design resumability around the interruption you need to survive](#react-file-uploads-with-uppy-section-5)
6. [Hand transferred files to an asynchronous media workflow](#react-file-uploads-with-uppy-section-6)
7. [Test lifecycle and failure behavior, not only the happy path](#react-file-uploads-with-uppy-section-7)

## What matters most

* Fetch short-lived signed Assembly options from an authenticated server endpoint; never expose an Auth Secret in React code.
* Use bounded retries for transient failures, make cancellation explicit, and add persisted recovery only when reload recovery is a real requirement.
* Store the Assembly ID and reconcile processing independently when the upload component may disappear before the workflow finishes.

## Define what “complete” means before writing the component

A browser can finish sending bytes while the resulting media is still being inspected, transformed, and exported. Decide which state the interface calls complete: file selection, transfer, Assembly creation, processing, durable storage, or publication in the application. The three tiers below group file selection and transfer under transfer success, Assembly creation and processing under processing success, and durable storage and publication under application success. A React component may display several of these states, but it should not collapse them into one success flag.

For a Transloadit workflow, Uppy owns the browser-side queue and transfer state. The Transloadit plugin creates an Assembly and associates each local file with that workflow. Your application should retain the Assembly ID as soon as it exists, then reconcile the terminal result outside the component when processing can outlive the page. A completed progress bar is not a durable asset record.

### Transfer success

The receiver accepted the file bytes. This is the state represented by upload progress when the plugin does not wait for encoding.

### Processing success

The Assembly reached a terminal success state and produced the expected result Steps.

### Application success

The application stored the Assembly and asset identifiers, confirmed ownership, and made the result available according to its product rules.

## Create one configured Uppy instance per mounted uploader

Install Core, Dashboard, the React interface package, the maintained Transloadit plugin, and the Zod schema validator for the authorization response. Import each Uppy stylesheet once from a stable entry point so its load order stays predictable. The component example below imports the package styles directly; move those imports to the application entry point if that is where your framework or bundler owns global styles.

Construct Uppy once for the lifetime of the mounted form. A lazy `useState` initializer in the next section gives each mounted uploader its own instance without rebuilding it on rerenders. Do not create a shared module-level singleton unless every surface is intentionally one queue: independent forms would otherwise see and remove one another’s files.

The asynchronous `assemblyOptions` function asks a trusted endpoint for authorization immediately before upload. The endpoint must authenticate and authorize the current user, apply abuse controls, choose a constrained Template, and return a short-lived signed payload. The browser validates the response shape but never receives the Auth Secret.

Install the React interface and Transloadit integration

```
yarn add @uppy/core @uppy/dashboard @uppy/react @uppy/transloadit zod
```

Create a constrained Uppy instance outside the render path

```
import Uppy from '@uppy/core'
import Transloadit from '@uppy/transloadit'
import { z } from 'zod'

const assemblyOptionsSchema = z.object({
  params: z.string().min(1),
  signature: z.string().regex(/^(sha1|sha256|sha384):[0-9a-f]+$/),
})

async function fetchAssemblyOptions(): Promise<z.infer<typeof assemblyOptionsSchema>> {
  const response = await fetch('/api/transloadit-params', {
    method: 'POST',
    headers: { Accept: 'application/json' },
  })

  if (!response.ok) {
    throw new Error('Could not authorize this upload')
  }

  const responseBody: unknown = await response.json().catch(() => null)
  const parsedOptions = assemblyOptionsSchema.safeParse(responseBody)
  if (!parsedOptions.success) {
    throw new Error('Could not authorize this upload')
  }

  return parsedOptions.data
}

export function createImageUploader(): Uppy {
  return new Uppy({
    autoProceed: false,
    restrictions: {
      allowedFileTypes: ['image/jpeg', 'image/png', 'image/webp'],
      maxFileSize: 50 * 1024 * 1024,
      maxNumberOfFiles: 5,
    },
  }).use(Transloadit, {
    assemblyOptions: fetchAssemblyOptions,
    retryDelays: [0, 1_000, 3_000, 5_000],
    waitForEncoding: false,
  })
}
```

## Render progress, cancellation, and Assembly state from Uppy

Uppy is an external state store. `useUppyState` subscribes React to selected values without maintaining a second queue in component state, while `useUppyEvent` exposes events that are not durable store fields. The component reads file count, aggregate progress, active upload state, the raw error state, and the Assembly-created event. It maps the error to a stable user-facing message instead of displaying the raw value, and it does not mirror individual files into a separate `useState` value.

Dashboard provides file selection, drag-and-drop, previews for supported local files, per-file status, and upload controls. The separate live region gives the surrounding application a concise status announcement, and the native disabled button exposes whether cancellation is available. Keep the Dashboard’s own labels localized when the product supports multiple languages; the surrounding heading, status, and errors need the same treatment.

Destroy the Uppy instance when its owning component unmounts. Destruction cancels current work, removes installed plugins, and releases listeners. A route change therefore should not be the only copy of important progress: retain the Assembly ID and any application task record before depending on work that may continue elsewhere.

Render Uppy state in React and clean up the instance

```
import type { ReactNode } from 'react'

import Dashboard from '@uppy/react/dashboard'
import { useUppyEvent, useUppyState } from '@uppy/react'
import { useEffect, useState } from 'react'

import { createImageUploader } from './createImageUploader.ts'

import '@uppy/core/css/style.min.css'
import '@uppy/dashboard/css/style.min.css'

export function ReactFileUploader(): ReactNode {
  const [uppy] = useState(createImageUploader)
  const error = useUppyState(uppy, (state) => state.error)
  const fileCount = useUppyState(uppy, (state) => Object.keys(state.files).length)
  const isUploading = useUppyState(
    uppy,
    (state) => Object.keys(state.currentUploads).length > 0,
  )
  const progress = useUppyState(uppy, (state) => state.totalProgress)
  const [assemblyCreatedArgs, clearAssemblyCreated] = useUppyEvent(
    uppy,
    'transloadit:assembly-created',
  )
  useUppyEvent(uppy, 'cancel-all', clearAssemblyCreated)
  const [assembly] = assemblyCreatedArgs
  const assemblyId = assembly?.assembly_id

  useEffect(() => {
    return () => uppy.destroy()
  }, [uppy])

  let status = 'Choose up to five JPEG, PNG, or WebP images.'
  if (fileCount > 0) status = 'Ready to upload.'
  if (isUploading) status = `Upload ${progress}% complete.`
  if (!isUploading && progress === 100) status = 'Files transferred. Processing may continue.'
  if (error != null) status = 'Upload failed. Check the selected files and try again.'

  return (
    <section aria-labelledby="file-upload-heading">
      <h2 id="file-upload-heading">Upload images</h2>
      <Dashboard height={420} uppy={uppy} />
      <p aria-live="polite" role="status">
        {status}
      </p>
      {assemblyId != null ? (
        <p>
          Processing reference: <code>{assemblyId}</code>
        </p>
      ) : null}
      <button disabled={fileCount === 0} onClick={() => uppy.cancelAll()} type="button">
        Cancel and remove files
      </button>
    </section>
  )
}
```

## Use previews and restrictions as interface features, not trust boundaries

A local preview helps a person catch the wrong selection before paying the upload cost. It is not the processed output and does not prove that the file is safe, decodable, correctly oriented, or honestly labeled. Keep preview work bounded because decoding many large images consumes browser memory. For formats the browser cannot preview, show the filename, declared type, and size without inventing a thumbnail.

Uppy restrictions reject obvious mistakes early: allowed types, individual size, aggregate size, and file count. Repeat the same limits at a trusted receiver or in the saved Template because a caller can bypass React and modify file metadata. Use detected content and a processing attempt where appropriate, then store only accepted results. Keep `allow_steps_override` disabled when the browser must not replace the approved workflow. When a preview or restriction rejects a selection, show a stable user-facing message and keep raw provider responses, stack traces, credentials, and storage diagnostics out of the page.

### Fast feedback

Explain the accepted formats, count, and size before selection, then let Uppy reject known violations next to the control.

### Authoritative policy

Enforce authorization, byte limits, detected-content rules, processing bounds, and export destinations after browser code can no longer be trusted.

### Safe failures

Catch authorization and upload failures at the trusted boundary, map them to one stable message for the user, and route raw diagnostics only to server-side logs.

## Design resumability around the interruption you need to survive

The Transloadit plugin uploads local files through tus, which can continue a failed transfer from a server-confirmed offset while the upload resource remains valid. `retryDelays` handles a bounded set of transient failures during the current Uppy lifetime. Cancellation is different: `cancelAll()` intentionally aborts current work, removes the files, and resets upload state, so the interface should say that the selection will be removed.

A page reload destroys in-memory React and Uppy state. Reload recovery requires persisted client state and compatible server upload resources, such as a deliberately configured Golden Retriever plugin workflow. Test that behavior with the actual Transloadit integration before promising it. Persisted local metadata can become stale, sensitive, or inconsistent with expired authorization, so define retention and a way to discard unrecoverable entries.

Resumability also has an operational deadline. An Assembly cannot remain uploadable forever, and short-lived signed parameters can expire before a delayed retry starts. Distinguish automatic retry, pause and resume, reload restoration, and starting a new Assembly; they solve different failures and may reuse different identifiers.

## Hand transferred files to an asynchronous media workflow

A saved Template should describe the allowed processing graph: `/upload/handle` receives the browser files, `/file/filter` can reject unsupported observed inputs, transformation Robots produce bounded derivatives, and storage Robots export approved results when durable storage is part of the workflow. The signed request selects that Template; React does not construct arbitrary Steps or carry permanent storage credentials.

Choose `waitForEncoding` from the interface contract. Setting it to false lets the browser finish after transfer and is appropriate when the application records the Assembly ID, shows a separate processing state, and learns the terminal result from a verified Assembly Notification or a later Assembly Status lookup. Waiting for encoding can keep the interface aligned with short processing, but it does not replace durable reconciliation if the tab closes.

Store application-level context next to the Assembly ID: authenticated user or tenant, intended asset slot, Template ID, creation time, and the application operation that caused it. When a notification arrives, verify its signature, handle duplicate delivery idempotently, confirm that the Assembly belongs to the expected record, and save only the result fields the product needs.

## Test lifecycle and failure behavior, not only the happy path

Exercise the component with a permitted small file, an oversized file, a misleading extension, an unsupported type, several files at the count boundary, a zero-byte input, a slow connection, an offline interval, server rejection, authorization expiry, user cancellation, component unmount, and a page reload. Confirm which state is retained, which work is aborted, and which message a keyboard or screen-reader user receives.

Test the real signing and processing path in addition to isolated React behavior. A mock can prove that the button disables or a status changes, but only an integrated fixture proves that the signed parameters match, tus resumes from the expected offset, the Template rejects bad content, the Assembly ID is recorded, and completion is reconciled once. Keep fixture files small and remove temporary production data after the run.

### React lifecycle

Rerender without replacing the Uppy instance, then unmount and verify that plugins and active browser work are cleaned up.

### Accessible interaction

Select files without drag-and-drop, operate every control by keyboard, and verify that rejection, progress, cancellation, and completion are announced in text.

### Processing reconciliation

Close the page after Assembly creation, deliver a repeated completion notification, and prove that one application asset reaches the correct terminal state.

## Technical details worth knowing

* Uppy is a stateful external store. Constructing it in the render body creates a new, discarded instance on every render and loses queued files and upload state. Keeping one instance but rerunning its setup can instead register duplicate plugins and listeners.
* The `useUppyState` hook subscribes to the Uppy store through React’s external-store contract and selects only the state a component needs.
* The React Dashboard installs its interface plugin when mounted and removes that interface plugin when unmounted; the application remains responsible for destroying the Uppy instance it created.
* Uppy restrictions reject disallowed selections in the browser, but callers can bypass browser code and declared MIME types can be wrong, so trusted validation remains necessary.
* The Transloadit plugin creates an Assembly and uploads local files to its tus endpoint. Its asynchronous `assemblyOptions` callback can obtain signed parameters immediately before an upload begins.
* The `retryDelays` option retries transient tus upload failures while the Uppy instance and upload state remain available; it does not by itself restore a transfer after a reload or closed tab.
* Calling `cancelAll()` emits cancellation, aborts active work through installed uploaders, removes the current files, and resets Uppy’s upload state.
* With `waitForEncoding` set to false, the Uppy upload can complete after the files transfer while the Assembly continues processing. Persist the Assembly ID and use verified notifications or an Assembly Status lookup for durable completion.

## A practical approach

1. 1\
   Define the accepted files, byte destination, processing Template, and terminal application state before building the component.
2. 2\
   Create one configured Uppy instance, render Dashboard and accessible status from its store, and clean it up on unmount.
3. 3\
   Issue constrained signed Assembly options on the server and repeat file limits and observed-content checks in the Template.
4. 4\
   Test rejection, interruption, cancellation, retry, unmount, reload, expired authorization, and asynchronous completion.

A four-stage media workflow

## When Transloadit is useful

Use Uppy’s maintained React Dashboard and Transloadit plugin when a React application needs a polished upload interface, resumable tus transfer, and managed server-side validation and transformation Steps in a saved Template. Fetch short-lived signed Assembly options from a trusted server and retain each Assembly ID when processing can outlive the page.

## Architecture boundary

React owns component lifetime and renders upload state. Uppy owns browser file selection, previews, restrictions, transfer state, and retry or cancellation controls. Transloadit authorizes and executes the saved media workflow. The application still owns user permission, durable asset records, publication policy, and reconciliation after the component unmounts.

## Frequently asked questions

### Should Uppy be created inside a React component?

Yes, when that component owns the queue, but create it with a lazy state initializer so rerenders reuse one instance. Destroy the instance on unmount. Use a shared provider only when several components intentionally operate on the same uploader.

### Does the Transloadit plugin need a separate Uppy Tus plugin?

No for local files sent to Transloadit. The Transloadit plugin configures tus uploads to the Assembly endpoint. Use the standalone Tus plugin when the destination is a separate tus server rather than a Transloadit Assembly.

### Are Uppy file restrictions secure?

No. They improve browser feedback, but requests can bypass React and file metadata can be false. Repeat authorization, byte limits, observed-content validation, workflow constraints, and storage policy at trusted boundaries.

### Does an Uppy image preview resize the uploaded file?

No. A preview is browser interface state. Preserve the selected source and create reproducible derivatives in the processing workflow unless the product deliberately implements a separate client-side preprocessing step.

### Will retryDelays resume an upload after a page reload?

No. Retry delays cover transient failures while the current uploader state remains available. Reload recovery needs persisted client metadata and a valid server upload resource, and it should be tested as a separate feature.

### When should waitForEncoding be enabled?

Enable it when the mounted interface must wait for short processing and display those results directly. Leave it disabled when transfer should finish promptly, then retain the Assembly ID and reconcile processing through verified notifications or status checks.

## Build the workflow

Move from the concept to a tested Assembly with Robot documentation and working demos.

* [Sign browser-created Assembly requests](/docs/api/authentication.md)
* [Understand resumable uploads](/docs/api/resumable-uploads.md)
* [Read the Uppy React documentation⁠](https://uppy.io/docs/react/)
* [Configure the Uppy Transloadit plugin⁠](https://uppy.io/docs/transloadit/)
* [Evaluate Golden Retriever reload recovery⁠](https://uppy.io/docs/golden-retriever/)
* [Reconcile processing with Assembly Notifications](/docs/topics/webhooks.md)
* [Interpret the Assembly Status response](/docs/api/assembly-status-response.md)
* [Read the API documentation](/docs.md)
* [Explore working demos](/demos.md)
* [Create a free workspace](/c/signup/)

Uploads and frontend integration

## Continue with related guides

* [File Upload API guide: architecture, security, and provider selection](/guides/file-upload-api-guide.md)\
  Choose and implement a File Upload API by comparing architecture, resumability, direct-to-cloud transfer, security, storage boundaries, and providers.
* [Secure file uploads in Next.js with Uppy and signed Transloadit Templates](/guides/secure-file-uploads-nextjs-uppy.md)\
  Build a secure Next.js App Router upload with Uppy, server-only signing, a locked Template, authoritative validation, resumable transfer, and asynchronous completion.
* [How to accept large uploads that survive a dropped connection](/guides/resumable-uploads-for-large-files.md)\
  Accept multi-gigabyte uploads over tus, continue them after a dropped connection, and keep the Assembly alive long enough to finish.
* [Best JavaScript file upload libraries: Uppy vs FilePond vs Dropzone](/guides/best-javascript-file-upload-libraries.md)\
  Compare Uppy, FilePond, and Dropzone by transfer protocol, interface model, recovery behavior, integration work, and long-term ownership.
* [HTML video in production: 10 practical checks](/guides/html-video-production-checklist.md)\
  Ten production checks for HTML video, from source selection and captions to poster images and preprocessing.
* [Three best practices for CSS banner images](/guides/css-banner-image-best-practices.md)\
  Three durable practices for sharp, responsive CSS banner images without unreadable text or accidental crops.
