# EdgeStore Docs: Uploader Provider
URL: https://edgestore.dev/docs/components/uploader-provider
Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/components/uploader-provider.mdx
import { LimitedCode } from '@/components/ui/limited-code';
import {
OpenTabs,
OpenTabsContent,
OpenTabsList,
OpenTabsTrigger,
} from '@/components/ui/open-tabs';
import { Callout } from 'fumadocs-ui/components/callout';
If you are installing the other dropzone components via the CLI, this
component will be installed automatically. You can skip the following steps.
## Installation
CLI
Manual
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://edgestore.dev/r/uploader-provider.json
```
```bash
pnpm dlx shadcn@latest add https://edgestore.dev/r/uploader-provider.json
```
```bash
yarn dlx shadcn@latest add https://edgestore.dev/r/uploader-provider.json
```
```bash
bun x shadcn@latest add https://edgestore.dev/r/uploader-provider.json
```
### Copy this component
````tsx title="components/upload/uploader-provider.tsx"
'use client';
import * as React from 'react';
/**
* Represents the possible statuses of a file in the uploader.
*/
export type FileStatus = 'PENDING' | 'UPLOADING' | 'COMPLETE' | 'ERROR';
/**
* Represents the state of a file in the uploader.
*/
export type FileState = {
/** The file object being uploaded */
file: File;
/** Unique identifier for the file */
key: string;
/** Upload progress (0-100) */
progress: number;
/** Current status of the file */
status: FileStatus;
/** URL of the uploaded file (available when status is COMPLETE) */
url?: string;
/** Error message if the upload failed */
error?: string;
};
/**
* Represents a file that has completed uploading.
*/
export type CompletedFileState = Omit & {
/** Status is guaranteed to be 'COMPLETE' */
status: 'COMPLETE';
/** URL is guaranteed to be available */
url: string;
};
/**
* Function type for handling file uploads.
*/
export type UploadFn = (props: {
/** The file to be uploaded */
file: File;
/** AbortSignal to cancel the upload */
signal: AbortSignal;
/** Callback to update progress */
onProgressChange: (progress: number) => void | Promise;
/** Additional options */
options?: TOptions;
}) => Promise<{ url: string }>;
/**
* Context type for the UploaderProvider.
*/
type UploaderContextType = {
/** List of all files in the uploader */
fileStates: FileState[];
/** Add files to the uploader. Returns the new file states. */
addFiles: (files: File[]) => FileState[];
/** Update a file's state */
updateFileState: (key: string, changes: Partial) => void;
/** Remove a file from the uploader, aborting its upload if it is running */
removeFile: (key: string) => void;
/**
* Cancel an ongoing upload. With `autoUpload` the file is removed,
* otherwise it goes back to `PENDING`.
*/
cancelUpload: (key: string) => void;
/**
* Upload files that are `PENDING` or `ERROR` (so it also retries failed uploads).
* Pass keys to upload only those files.
*/
uploadFiles: (keysToUpload?: string[], options?: TOptions) => Promise;
/** Remove all files, aborting any running uploads */
resetFiles: () => void;
/** Whether any file is currently uploading */
isUploading: boolean;
/** Whether files are uploaded as soon as they are added */
autoUpload: boolean;
};
/**
* Props for the UploaderProvider component.
*/
type ProviderProps = {
/** React children or render function */
children:
| React.ReactNode
| ((context: UploaderContextType) => React.ReactNode);
/** Callback when files change */
onChange?: (args: {
allFiles: FileState[];
completedFiles: CompletedFileState[];
}) => void | Promise;
/** Callback when a file is added */
onFileAdded?: (file: FileState) => void | Promise;
/** Callback when a file is removed */
onFileRemoved?: (key: string) => void | Promise;
/** Callback when a file upload completes */
onUploadCompleted?: (file: CompletedFileState) => void | Promise;
/** Function to handle the actual upload */
uploadFn: UploadFn;
/** External value to control the file states */
value?: FileState[];
/** Whether files should be automatically uploaded when added */
autoUpload?: boolean;
};
const UploaderContext =
React.createContext | null>(null);
/**
* Hook to access the uploader context.
*
* @throws Error if used outside of UploaderProvider
*
* @example
* ```tsx
* const { fileStates, addFiles, uploadFiles } = useUploader();
* ```
*/
export function useUploader() {
const context = React.useContext(UploaderContext);
if (!context) {
throw new Error('useUploader must be used within a UploaderProvider');
}
return context as UploaderContextType;
}
/**
* Holds the files of an uploader and runs their uploads.
*
* @example
* ```tsx
* {
* // Upload implementation
* return { url: 'https://example.com/uploads/image.jpg' };
* }}
* autoUpload
* >
*
*
* ```
*/
export function UploaderProvider({
children,
onChange,
onFileAdded,
onFileRemoved,
onUploadCompleted,
uploadFn,
value: externalValue,
autoUpload = false,
}: ProviderProps) {
const [fileStates, setFileStates] = React.useState(
externalValue ?? [],
);
// Abort controllers of running uploads, by file key.
const controllers = React.useRef(new Map());
// Sync with external value if provided
React.useEffect(() => {
if (externalValue) {
setFileStates(externalValue);
}
}, [externalValue]);
const updateFileState = React.useCallback(
(key: string, changes: Partial) => {
setFileStates((prev) =>
prev.map((fileState) =>
fileState.key === key ? { ...fileState, ...changes } : fileState,
),
);
},
[],
);
const upload = React.useCallback(
async (fileState: FileState, options?: TOptions) => {
const { key, file } = fileState;
const controller = new AbortController();
controllers.current.set(key, controller);
updateFileState(key, {
status: 'UPLOADING',
progress: 0,
error: undefined,
});
try {
const { url } = await uploadFn({
file,
signal: controller.signal,
onProgressChange: (progress) => {
if (!controller.signal.aborted) updateFileState(key, { progress });
},
options,
});
// Let the progress bar reach 100% before showing the completed state.
await new Promise((resolve) => setTimeout(resolve, 500));
if (controller.signal.aborted) return;
updateFileState(key, { status: 'COMPLETE', progress: 100, url });
void onUploadCompleted?.({
...fileState,
status: 'COMPLETE',
progress: 100,
url,
error: undefined,
});
} catch (err: unknown) {
// cancelUpload/removeFile already updated the state.
if (controller.signal.aborted) return;
if (process.env.NODE_ENV === 'development') {
console.error(err);
}
updateFileState(key, {
status: 'ERROR',
error: err instanceof Error ? err.message : 'Upload failed',
});
} finally {
if (controllers.current.get(key) === controller) {
controllers.current.delete(key);
}
}
},
[updateFileState, uploadFn, onUploadCompleted],
);
const uploadFiles = React.useCallback(
async (keysToUpload?: string[], options?: TOptions) => {
const filesToUpload = fileStates.filter(
(fileState) =>
(fileState.status === 'PENDING' || fileState.status === 'ERROR') &&
(!keysToUpload || keysToUpload.includes(fileState.key)),
);
await Promise.all(
filesToUpload.map((fileState) => upload(fileState, options)),
);
},
[fileStates, upload],
);
const addFiles = React.useCallback(
(files: File[]) => {
if (files.length === 0) return [];
const newFileStates = files.map((file) => ({
file,
key: `${file.name}-${Date.now()}-${Math.random().toString(36).slice(2)}`,
progress: 0,
status: 'PENDING',
}));
setFileStates((prev) => [...prev, ...newFileStates]);
newFileStates.forEach((fileState) => {
void onFileAdded?.(fileState);
if (autoUpload) void upload(fileState);
});
return newFileStates;
},
[autoUpload, onFileAdded, upload],
);
const removeFile = React.useCallback(
(key: string) => {
controllers.current.get(key)?.abort();
setFileStates((prev) =>
prev.filter((fileState) => fileState.key !== key),
);
void onFileRemoved?.(key);
},
[onFileRemoved],
);
const cancelUpload = React.useCallback(
(key: string) => {
const controller = controllers.current.get(key);
if (!controller) return;
controller.abort();
if (autoUpload) {
removeFile(key);
} else {
updateFileState(key, { status: 'PENDING', progress: 0 });
}
},
[autoUpload, removeFile, updateFileState],
);
const resetFiles = React.useCallback(() => {
controllers.current.forEach((controller) => controller.abort());
setFileStates([]);
}, []);
// Abort running uploads when the provider unmounts.
React.useEffect(() => {
const running = controllers.current;
return () => {
running.forEach((controller) => controller.abort());
};
}, []);
React.useEffect(() => {
const completedFiles = fileStates.filter(
(fs): fs is CompletedFileState => fs.status === 'COMPLETE' && !!fs.url,
);
void onChange?.({ allFiles: fileStates, completedFiles });
}, [fileStates, onChange]);
const isUploading = fileStates.some((fs) => fs.status === 'UPLOADING');
const value = React.useMemo(
() => ({
fileStates,
addFiles,
updateFileState,
removeFile,
cancelUpload,
uploadFiles,
resetFiles,
isUploading,
autoUpload,
}),
[
fileStates,
addFiles,
updateFileState,
removeFile,
cancelUpload,
uploadFiles,
resetFiles,
isUploading,
autoUpload,
],
);
return (
}>
{typeof children === 'function' ? children(value) : children}
);
}
````
## Usage
Install or copy the component from [Installation](#installation) before using these examples.
This section provides a step-by-step guide on how to use the `UploaderProvider` and the `useUploader` hook.
### 1. Setup ``
Wrap the part of your application that needs uploader functionality with `UploaderProvider`. You must provide an `uploadFn` and can optionally configure `autoUpload`.
* **`uploadFn`**: An asynchronous function that handles the actual file upload. It receives the `file`, an `onProgressChange` callback, and an `AbortSignal`. It should return an object with the uploaded file's `url`.
* **`autoUpload`**: (Optional, default: `false`) If `true`, files will start uploading immediately after being added.
```tsx
import { UploaderProvider, UploadFn } from '@/components/upload/uploader-provider';
import { useEdgeStore } from '@/lib/edgestore'; // Adjust import path
import * as React from 'react';
function MyUploaderPage() {
const { edgestore } = useEdgeStore();
// Define the upload function
const uploadFn: UploadFn = React.useCallback(
async ({ file, onProgressChange, signal }) => {
// Example using Edge Store client
const res = await edgestore.publicFiles.upload({
file,
signal,
onProgressChange,
});
// you can run some server action or api here
// to add the necessary data to your database
console.log('Upload successful:', res);
return res; // Must return { url: string }
},
[edgestore],
);
return (
// Provide the uploadFn and configure autoUpload
{/* Your uploader components go here */}
);
}
// export default MyUploaderPage; // Assuming MyUploaderComponent is defined below
```
### 2. Use the `useUploader` Hook
Inside components nested under `UploaderProvider`, use the `useUploader` hook to access the uploader's state and control functions.
```tsx
import { useUploader } from '@/components/upload/uploader-provider';
import * as React from 'react';
function MyUploaderComponent() {
const {
fileStates, // Array of current file states
addFiles, // Function to add files
removeFile, // Function to remove a file by key
cancelUpload, // Function to cancel an upload by key
uploadFiles, // Function to trigger uploads (all pending or specific keys)
isUploading, // Boolean indicating if any upload is in progress
} = useUploader();
// ... component logic using these values and functions ...
return
{/* UI elements */}
;
}
```
### 3. Adding Files (`addFiles`)
Typically, you'll use a standard file input. You might hide it and trigger its click event from a custom button. Get the selected `File` objects from the input's `onChange` event and pass them to `addFiles`.
```tsx
function MyUploaderComponent() {
const { addFiles } = useUploader();
const inputRef = React.useRef(null);
// Handle file selection from the input
const handleFileChange = (e: React.ChangeEvent) => {
if (e.target.files) {
addFiles(Array.from(e.target.files));
// Optional: Reset input value to allow selecting the same file again
e.target.value = '';
}
};
// Trigger the hidden input click
const handleAddClick = () => {
inputRef.current?.click();
};
return (
{/* Hidden file input */}
{/* Button to open file selector */}
{/* ... rest of the component ... */}
);
}
```
### 4. Displaying File State (`fileStates`)
The `fileStates` array contains objects representing each file. Each object includes:
* `file`: The original `File` object.
* `key`: A unique string identifier.
* `status`: `'PENDING'`, `'UPLOADING'`, `'COMPLETE'`, or `'ERROR'`.
* `progress`: Upload progress (0-100).
* `url`: (Optional) The URL after successful upload (`status === 'COMPLETE'`).
* `error`: (Optional) Error message if upload failed (`status === 'ERROR'`).
Iterate over `fileStates` to render the UI for each file.
```tsx
function MyUploaderComponent() {
const { fileStates, removeFile, cancelUpload } = useUploader();
return (
{/* ... Add files button/input ... */}
{/* List of files */}
{fileStates.length > 0 && (
{fileStates.map((fileState) => (
{fileState.file.name} ({fileState.status})
{/* Show progress during upload */}
{fileState.status === 'UPLOADING' && (
{fileState.progress}%
)}
{/* Show cancel button during upload */}
{fileState.status === 'UPLOADING' && (
)}
{/* Show remove button otherwise */}
{fileState.status !== 'UPLOADING' && (
)}
{/* Show error message */}
{fileState.status === 'ERROR' && (
{' '}
Error: {fileState.error}
)}
{/* Show link on completion */}
{fileState.status === 'COMPLETE' && fileState.url && (
View File
)}
))}
)}
);
}
```
### 5. Triggering Uploads (`uploadFiles`)
Call `uploadFiles()` to upload all files with status `'PENDING'` or `'ERROR'`. You can optionally pass an array of specific file keys to `uploadFiles(keysToUpload)` to upload only those files. Use the `isUploading` boolean to disable the upload button during active uploads.
Since failed files are included, `uploadFiles([fileState.key])` is also how you retry a single failed upload:
```tsx
{
fileState.status === 'ERROR' && (
);
}
```
```tsx
function MyUploaderComponent() {
const { uploadFiles, isUploading, fileStates } = useUploader();
// Check if there are any files pending upload
const hasPendingFiles = fileStates.some((fs) => fs.status === 'PENDING');
return (
{/* ... Add files button/input and file list ... */}
{/* Upload button */}
);
}
```
### 6. Cancelling Uploads (`cancelUpload`)
Call `cancelUpload(key)` with the file's unique key to abort an ongoing upload. With `autoUpload`, the file is removed. Otherwise it goes back to `'PENDING'` so it can be uploaded again. Your `uploadFn` must pass the `AbortSignal` on (EdgeStore's `upload` accepts it as `signal`) for cancellation to stop the request.
```tsx
// Example within the file list rendering (see step 4)
{
fileState.status === 'UPLOADING' && (
);
}
```
### 7. Removing Files (`removeFile`)
Call `removeFile(key)` with the file's key to remove it from the list, regardless of its status. If the file is currently uploading, the upload is aborted. Removing a completed file only removes it from the uploader; it doesn't delete it from storage. Use `onFileRemoved` if you need to do that.
```tsx
// Example within the file list rendering (see step 4)
{
;
}
```
### 8. Callbacks
You can pass callback props (`onChange`, `onFileAdded`, `onFileRemoved`, `onUploadCompleted`) to the `UploaderProvider` to execute logic when the uploader state changes.
```tsx
{
console.log('Files changed:', allFiles);
console.log('Completed files:', completedFiles);
}}
onFileAdded={(fileState) => console.log('File added:', fileState.file.name)}
onUploadCompleted={(completedFile) =>
console.log('Upload complete:', completedFile.url)
}
>
{/* ... */}
```
### Complete Component Example (`MyUploaderComponent`)
Here is the `MyUploaderComponent` combining the steps above:
```tsx
import { useUploader } from '@/components/upload/uploader-provider';
import * as React from 'react';
function MyUploaderComponent() {
const {
fileStates,
addFiles,
removeFile,
cancelUpload,
uploadFiles,
isUploading,
} = useUploader();
const inputRef = React.useRef(null);
// Function to handle file selection
const handleFileChange = (e: React.ChangeEvent) => {
if (e.target.files) {
addFiles(Array.from(e.target.files));
e.target.value = ''; // Reset input
}
};
// Function to trigger the hidden file input
const handleAddClick = () => {
inputRef.current?.click();
};
const hasPendingFiles = fileStates.some((fs) => fs.status === 'PENDING');
return (
);
}
```
### Putting It All Together (`MyUploaderPage`)
Finally, use the `MyUploaderComponent` within the page component wrapped by the `UploaderProvider`.
```tsx
import { UploaderProvider, UploadFn } from '@/components/upload/uploader-provider';
import { useEdgeStore } from '@/lib/edgestore'; // Adjust import path
import * as React from 'react';
// Assume MyUploaderComponent is defined in the same file or imported
// import { MyUploaderComponent } from './MyUploaderComponent';
function MyUploaderPage() {
const { edgestore } = useEdgeStore();
// Define the upload function (same as in step 1)
const uploadFn: UploadFn = React.useCallback(
async ({ file, onProgressChange, signal }) => {
const res = await edgestore.publicFiles.upload({
file,
signal,
onProgressChange,
});
console.log('Upload successful:', res);
return res;
},
[edgestore],
);
return (
My File Uploader
{
console.log(
`File ${completedFile.file.name} uploaded successfully to ${completedFile.url}`,
);
// Maybe trigger a database update here
}}
>
);
}
export default MyUploaderPage;
```
This provides a basic but functional file uploader using the context provider. You can style the elements and integrate them further into your application's UI.