Skip to content

React

The React client provides hooks, components, and utilities for building file upload UIs in React applications. Supports single/multi-file uploads, drag-and-drop, resumable uploads, and real-time progress tracking.

npm install @uploadista/react
import { UploadistaProvider, useUpload } from "@uploadista/react";
function App() {
return (
<UploadistaProvider
baseUrl="https://api.example.com"
storageId="my-storage"
chunkSize={1024 * 1024}
>
<UploadComponent />
</UploadistaProvider>
);
}

The React client supports two authentication modes that integrate with your existing auth system.

Use direct mode when you have your own authentication (JWT, API keys, sessions, OAuth):

import { UploadistaProvider } from "@uploadista/react";
function App() {
return (
<UploadistaProvider
baseUrl="https://api.example.com"
storageId="my-storage"
chunkSize={1024 * 1024}
auth={{
mode: 'direct',
getCredentials: async () => ({
headers: {
'Authorization': `Bearer ${await getAccessToken()}`
}
})
}}
>
<UploadComponent />
</UploadistaProvider>
);
}

Common patterns for direct mode:

// With OAuth token
auth={{
mode: 'direct',
getCredentials: async () => ({
headers: { 'Authorization': `Bearer ${await oauth.getAccessToken()}` }
})
}}
// With API key
auth={{
mode: 'direct',
getCredentials: () => ({
headers: { 'X-API-Key': process.env.REACT_APP_API_KEY }
})
}}
// With session cookie (browser automatically includes cookies)
auth={{
mode: 'direct',
getCredentials: () => ({
cookies: { 'session': getSessionId() }
})
}}

Use UploadistaCloud mode for automatic JWT token exchange with your auth server:

function App() {
return (
<UploadistaProvider
storageId="my-storage"
auth={{
mode: 'uploadista-cloud',
authServerUrl: '/api/auth/token',
clientId: process.env.REACT_APP_CLIENT_ID
}}
>
<UploadComponent />
</UploadistaProvider>
);
}
function UploadComponent() {
const upload = useUpload({
onProgress: (progress) => console.log(`Progress: ${progress}%`),
onSuccess: (result) => console.log("Complete:", result),
});
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) upload.upload(file);
};
return (
<div>
<input type="file" onChange={handleFileSelect} />
{upload.isUploading && (
<div>
<progress value={upload.state.progress} max={100} />
<span>{upload.state.progress}%</span>
</div>
)}
{upload.state.status === "success" && (
<p>Upload complete! File ID: {upload.state.result?.id}</p>
)}
</div>
);
}

Single file upload hook:

const upload = useUpload({
onProgress?: (progress, bytesUploaded, totalBytes) => void;
onSuccess?: (result: UploadFile) => void;
onError?: (error: Error) => void;
onChunkComplete?: (chunkSize, bytesAccepted, bytesTotal) => void;
onShouldRetry?: (error, retryAttempt) => boolean;
});
// Returns
upload.state // { status, progress, bytesUploaded, totalBytes, error, result }
upload.upload(file) // Start upload
upload.abort() // Cancel upload
upload.reset() // Reset state
upload.retry() // Retry failed upload
upload.isUploading // Boolean
upload.canRetry // Boolean
upload.metrics // Upload metrics

Multiple file upload hook:

const multiUpload = useMultiUpload({
maxConcurrent?: 3,
onComplete?: (results) => void,
});
// Returns
multiUpload.state // { progress, uploading, successful, failed }
multiUpload.items // Array of upload items
multiUpload.addFiles(files) // Add files to queue
multiUpload.startAll() // Start all uploads
multiUpload.retry(item) // Retry specific file
multiUpload.abort(item) // Cancel specific file
multiUpload.remove(item) // Remove from list
multiUpload.clear() // Clear all

Drag and drop functionality:

const dragDrop = useDragDrop({
accept?: ['image/*', '.pdf'],
maxFiles?: 5,
maxFileSize?: 10 * 1024 * 1024,
onFilesReceived?: (files) => void,
});
// Returns
dragDrop.state // { isDragging, errors }
dragDrop.dragHandlers // { onDragEnter, onDragLeave, onDragOver, onDrop }
dragDrop.openFilePicker() // Open file dialog
dragDrop.inputProps // Props for <input> element
import { useMultiUpload, useDragDrop } from "@uploadista/react";
function MultiUploadComponent() {
const multiUpload = useMultiUpload({
maxConcurrent: 3,
onComplete: (results) => {
console.log(`${results.successful.length}/${results.total} uploaded`);
},
});
const dragDrop = useDragDrop({
accept: ['image/*', '.pdf'],
maxFiles: 10,
maxFileSize: 50 * 1024 * 1024,
onFilesReceived: (files) => {
multiUpload.addFiles(files);
multiUpload.startAll();
},
});
return (
<div
{...dragDrop.dragHandlers}
style={{
border: dragDrop.state.isDragging ? "2px solid blue" : "2px dashed gray",
padding: "2rem",
textAlign: "center",
}}
>
{dragDrop.state.isDragging ? (
<p>Drop files here...</p>
) : (
<>
<p>Drag files here or</p>
<button onClick={dragDrop.openFilePicker}>Select Files</button>
<input {...dragDrop.inputProps} style={{ display: "none" }} />
</>
)}
<div style={{ marginTop: "2rem" }}>
<h3>Uploads ({multiUpload.items.length})</h3>
{multiUpload.items.map((item) => (
<div key={item.id} style={{ marginBottom: "1rem" }}>
<div>{item.file.name}</div>
<div>
<progress value={item.state.progress} max={100} />
<span> {item.state.progress}%</span>
</div>
<div>
Status: {item.state.status}
{item.state.status === "uploading" && (
<button onClick={() => multiUpload.abort(item)}>Cancel</button>
)}
{item.state.status === "error" && (
<button onClick={() => multiUpload.retry(item)}>Retry</button>
)}
</div>
</div>
))}
</div>
<div>
Overall: {multiUpload.state.progress}% |
Uploading: {multiUpload.state.uploading} |
Successful: {multiUpload.state.successful} |
Failed: {multiUpload.state.failed}
</div>
</div>
);
}
import { UploadZone } from "@uploadista/react/components";
<UploadZone onFilesSelected={(files) => handleUpload(files)}>
{({ isDragging, openFilePicker }) => (
<div onDragOver={...} onDrop={...}>
{isDragging ? "Drop here" : "Drag files or click"}
<button onClick={openFilePicker}>Browse</button>
</div>
)}
</UploadZone>
import { SimpleUploadZone } from "@uploadista/react/components";
<SimpleUploadZone
accept="image/*"
maxFiles={5}
maxFileSize={10 * 1024 * 1024}
onFilesSelected={(files) => handleUpload(files)}
/>

The Upload compound component provides a declarative, composable API for building upload UIs with full control over rendering:

import { Upload } from "@uploadista/react";
function FileUploader() {
return (
<Upload
multiple
maxConcurrent={3}
autoStart={false}
onComplete={(results) => console.log("All uploads complete:", results)}
>
{/* Drag & Drop Zone */}
<Upload.DropZone accept="image/*" maxFiles={10}>
{({ isDragging, getRootProps, getInputProps, openFilePicker }) => (
<div
{...getRootProps()}
style={{
border: `2px dashed ${isDragging ? "blue" : "gray"}`,
padding: "2rem",
textAlign: "center",
cursor: "pointer",
}}
>
<input {...getInputProps()} />
{isDragging ? "Drop files here..." : "Click or drag files to upload"}
<button type="button" onClick={openFilePicker}>
Browse Files
</button>
</div>
)}
</Upload.DropZone>
{/* Upload Items List */}
<Upload.Items>
{({ items }) =>
items.map((item) => (
<Upload.Item key={item.id} id={item.id}>
{({ file, state, abort, retry, remove }) => (
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
<span>{file.name}</span>
<progress value={state.progress} max={100} />
<span>{state.progress}%</span>
<span>{state.status}</span>
{state.status === "uploading" && (
<button onClick={abort}>Cancel</button>
)}
{state.status === "error" && (
<button onClick={retry}>Retry</button>
)}
<button onClick={remove}>Remove</button>
</div>
)}
</Upload.Item>
))
}
</Upload.Items>
{/* Global Progress */}
<Upload.Progress>
{({ progress, status }) => (
<div>
Overall: {progress}% - {status}
</div>
)}
</Upload.Progress>
</Upload>
);
}
ComponentDescription
Upload.DropZoneDrag & drop zone with file picker
Upload.ItemsContainer for listing upload items
Upload.ItemIndividual upload item with controls
Upload.ProgressGlobal upload progress
Upload.InputHidden file input (alternative to DropZone)
<Upload.DropZone
accept="image/*" // File type filter
maxFiles={10} // Maximum files to accept
maxFileSize={50 * 1024 * 1024} // Max file size in bytes
disabled={false} // Disable the drop zone
>
{({ isDragging, isOver, getRootProps, getInputProps, openFilePicker }) => (
// Render your custom UI
)}
</Upload.DropZone>
<Upload.Item id={item.id}>
{({
file, // The File object
state, // { status, progress, bytesUploaded, totalBytes, error, result }
abort, // Cancel this upload
retry, // Retry failed upload
remove, // Remove from list
pause, // Pause upload
resume, // Resume paused upload
}) => (
// Render your custom item UI
)}
</Upload.Item>
import {
formatFileSize,
formatSpeed,
formatDuration,
validateFileType,
isImageFile,
createFilePreview,
} from "@uploadista/react";
formatFileSize(1024000); // "1 MB"
formatSpeed(102400); // "100 KB/s"
formatDuration(65000); // "1m 5s"
validateFileType(file, ['image/*']); // true/false
isImageFile(file); // true/false
createFilePreview(file); // blob: URL

For advanced upload scenarios with processing pipelines, use the Flow component and useFlow hook. These provide built-in support for multi-input flows, auto-discovery of input nodes, and real-time flow execution tracking.

The useFlow hook is the primary way to interact with flows:

import { useFlow } from "@uploadista/react";
function FlowUploader() {
const flow = useFlow({
flowConfig: {
flowId: "image-optimization",
storageId: "s3-images",
},
onSuccess: (outputs) => {
console.log("Flow outputs:", outputs);
},
onError: (error) => {
console.error("Flow failed:", error);
},
onProgress: (uploadId, bytesUploaded, totalBytes) => {
console.log(`Progress: ${bytesUploaded}/${totalBytes}`);
},
});
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) flow.upload(file);
};
return (
<div>
<input type="file" onChange={handleFileSelect} />
{flow.isUploading && (
<div>
<p>Status: {flow.state.status}</p>
<progress value={flow.state.progress} max={100} />
{flow.state.currentNodeName && (
<p>Processing: {flow.state.currentNodeName}</p>
)}
</div>
)}
{flow.state.status === "success" && (
<div>
<p>Flow complete!</p>
<pre>{JSON.stringify(flow.state.flowOutputs, null, 2)}</pre>
</div>
)}
</div>
);
}
PropertyTypeDescription
stateFlowUploadStateComplete flow upload state
inputMetadataFlowInputMetadata[] | nullDiscovered input nodes (null until loaded)
inputStatesMap<string, InputExecutionState>Per-input execution states
inputsRecord<string, unknown>Current input values
setInput(nodeId, value) => voidSet value for a specific input
execute() => Promise<void>Execute flow with current inputs
upload(file) => Promise<void>Single file upload convenience method
abort() => voidCancel the current upload
pause() => voidPause the current upload
reset() => voidReset state and clear all inputs
isUploadingbooleanTrue when uploading or processing
isUploadingFilebooleanTrue only during file upload phase
isProcessingbooleanTrue only during flow processing phase
isDiscoveringInputsbooleanTrue while discovering flow inputs

For flows with multiple input nodes:

function MultiInputFlow() {
const flow = useFlow({
flowConfig: {
flowId: "multi-source-processing",
storageId: "default",
},
});
return (
<div>
{/* Show discovered inputs */}
{flow.inputMetadata?.map((input) => (
<div key={input.nodeId}>
<label>{input.nodeName}</label>
<p>{input.nodeDescription}</p>
{input.inputTypeId === "streaming-input-v1" ? (
<input
type="file"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) flow.setInput(input.nodeId, file);
}}
/>
) : (
<input
type="url"
placeholder="https://example.com/file"
onChange={(e) => flow.setInput(input.nodeId, e.target.value)}
/>
)}
</div>
))}
<button onClick={flow.execute} disabled={flow.isUploading}>
Execute Flow
</button>
{/* Show per-input progress */}
{flow.isUploading && (
<div>
{Array.from(flow.inputStates.entries()).map(([nodeId, state]) => (
<div key={nodeId}>
{nodeId}: {state.status} ({state.progress}%)
</div>
))}
</div>
)}
</div>
);
}

For declarative, composable flow UIs, use the Flow compound component. The Flow component works directly within UploadistaProvider - no additional providers are required:

import { UploadistaProvider, Flow } from "@uploadista/react";
function App() {
return (
<UploadistaProvider baseUrl="http://localhost:3000" storageId="local">
<ImageProcessor />
</UploadistaProvider>
);
}
function ImageProcessor() {
return (
<Flow
flowId="image-optimizer"
storageId="local"
onSuccess={(outputs) => console.log("Complete:", outputs)}
>
{/* Simple drop zone for single file */}
<Flow.DropZone accept="image/*">
{({ isDragging, progress, getRootProps, getInputProps }) => (
<div
{...getRootProps()}
style={{
border: isDragging ? "2px solid blue" : "2px dashed gray",
padding: "2rem",
}}
>
<input {...getInputProps()} />
{isDragging ? "Drop image here" : "Drag image or click to select"}
{progress > 0 && <progress value={progress} max={100} />}
</div>
)}
</Flow.DropZone>
{/* Status display */}
<Flow.Status>
{({ status, currentNodeName }) => (
<p>
Status: {status}
{currentNodeName && ` (Processing: ${currentNodeName})`}
</p>
)}
</Flow.Status>
{/* Error display */}
<Flow.Error>
{({ hasError, message, reset }) =>
hasError && (
<div style={{ color: "red" }}>
Error: {message}
<button onClick={reset}>Try Again</button>
</div>
)
}
</Flow.Error>
</Flow>
);
}

Drop zone for single-file uploads:

<Flow.DropZone accept="image/*" maxFileSize={10 * 1024 * 1024}>
{({ isDragging, isOver, progress, status, getRootProps, getInputProps, openFilePicker }) => (
// Render your drop zone UI
)}
</Flow.DropZone>

Auto-discovers and lists all input nodes:

<Flow.Inputs>
{({ inputs, isLoading }) =>
isLoading ? (
<Spinner />
) : (
inputs.map((input) => (
<Flow.Input key={input.nodeId} nodeId={input.nodeId}>
{/* Render input UI */}
</Flow.Input>
))
)
}
</Flow.Inputs>

Scoped context for a specific input node:

<Flow.Input nodeId="video-input">
{({ metadata, value, setValue, state }) => (
<div>
<label>{metadata.nodeName}</label>
<Flow.Input.DropZone accept="video/*">
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
Drop video here
</div>
)}
</Flow.Input.DropZone>
<Flow.Input.Preview>
{({ isFile, fileName, fileSize, clear }) =>
isFile && (
<div>
{fileName} ({fileSize} bytes)
<button onClick={clear}>Remove</button>
</div>
)
}
</Flow.Input.Preview>
</div>
)}
</Flow.Input>

URL input for remote files:

<Flow.Input nodeId="url-input">
<Flow.Input.UrlField placeholder="https://example.com/video.mp4" />
</Flow.Input>

Progress display:

<Flow.Progress>
{({ progress, bytesUploaded, totalBytes, status }) => (
<div>
<progress value={progress} max={100} />
<span>{bytesUploaded} / {totalBytes} bytes</span>
</div>
)}
</Flow.Progress>

Status display with node tracking:

<Flow.Status>
{({ status, currentNodeName, currentNodeType, flowOutputs }) => (
<div>
Status: {status}
{currentNodeName && <span>Processing: {currentNodeName}</span>}
{flowOutputs && <pre>{JSON.stringify(flowOutputs, null, 2)}</pre>}
</div>
)}
</Flow.Status>

Error handling:

<Flow.Error>
{({ hasError, error, message, reset }) =>
hasError && (
<div>
<p>Error: {message}</p>
<button onClick={reset}>Reset</button>
</div>
)
}
</Flow.Error>

Action buttons:

<Flow.Submit disabled={!hasAllInputs}>Execute Flow</Flow.Submit>
<Flow.Cancel>Cancel</Flow.Cancel>
<Flow.Reset>Start Over</Flow.Reset>

FlowManagerProvider is already included inside UploadistaProvider, so you don’t need to add it manually for basic usage. It’s only needed if you want to access the flow manager context directly for advanced scenarios like coordinating multiple concurrent flow instances:

import { useFlowManagerContext } from "@uploadista/react";
function AdvancedFlowList() {
// Access the flow manager for advanced multi-flow coordination
const { getManager, releaseManager } = useFlowManagerContext();
// getManager(flowId) - Get or create a flow manager instance
// releaseManager(flowId) - Release a flow manager when done
}

Note: For most use cases, simply use the Flow compound component or useFlow hook directly within UploadistaProvider.

For managing multiple concurrent flow uploads:

import { useMultiFlowUpload } from "@uploadista/react";
function BatchProcessor() {
const multiFlow = useMultiFlowUpload({
flowConfig: {
flowId: "image-optimization",
storageId: "s3",
},
maxConcurrent: 3,
});
return (
<div>
<input
type="file"
multiple
onChange={(e) => {
const files = Array.from(e.target.files ?? []);
files.forEach((file) => multiFlow.addUpload(file));
}}
/>
{multiFlow.uploads.map((upload) => (
<div key={upload.id}>
{upload.file.name}: {upload.state.progress}%
</div>
))}
</div>
);
}

Listen to flow execution events:

import { useFlowEvents } from "@uploadista/react";
function FlowMonitor() {
useFlowEvents({
onFlowStart: (event) => console.log("Started:", event.flowId),
onNodeStart: (event) => console.log("Node:", event.nodeName),
onNodeEnd: (event) => console.log("Node complete:", event.result),
onFlowEnd: (event) => console.log("Outputs:", event.outputs),
onFlowError: (event) => console.error("Error:", event.error),
});
return null;
}

Fully typed with TypeScript 5.9+:

import type {
UploadState,
UploadFile,
UploadMetrics,
FlowUploadState,
FlowUploadStatus,
FlowInputMetadata,
UseFlowReturn,
FlowProps,
FlowContextValue,
} from "@uploadista/react";
const state: UploadState = {
status: "uploading",
progress: 45,
bytesUploaded: 2359296,
totalBytes: 5242880,
error: null,
result: null,
};