Skip to main content

Uploader Provider

The Uploader Provider is a context provider that provides the uploader context to the components. You can use it as a base for creating your own components.

tip

If you are installing the other dropzone components via the CLI, this component will be installed automatically. You can skip the following steps.

Installation

bash
pnpm dlx shadcn@latest add https://edgestore.dev/r/uploader-provider.json

Usage

This section provides a step-by-step guide on how to use the UploaderProvider and the useUploader hook.

1. Setup <UploaderProvider>

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/ui/uploader'; // Adjust import path
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
<UploaderProvider uploadFn={uploadFn}>
{/* Your uploader components go here */}
<MyUploaderComponent />
</UploaderProvider>
);
}
// 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/ui/uploader'; // Adjust import path
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 (
<div>
{/* UI elements */}
</div>
);
}

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<HTMLInputElement>(null);
// Handle file selection from the input
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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 (
<div>
{/* Hidden file input */}
<input
type="file"
ref={inputRef}
onChange={handleFileChange}
multiple // Allow multiple files
style={{ display: 'none' }}
/>
{/* Button to open file selector */}
<button onClick={handleAddClick}>Add Files</button>
{/* ... rest of the component ... */}
</div>
);
}

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 (
<div>
{/* ... Add files button/input ... */}
{/* List of files */}
{fileStates.length > 0 && (
<ul>
{fileStates.map((fileState) => (
<li key={fileState.key}>
<span>{fileState.file.name}</span>
<span> ({fileState.status})</span>
{/* Show progress during upload */}
{fileState.status === 'UPLOADING' && (
<span> {fileState.progress}%</span>
)}
{/* Show cancel button during upload */}
{fileState.status === 'UPLOADING' && (
<button onClick={() => cancelUpload(fileState.key)}>
Cancel
</button>
)}
{/* Show remove button otherwise */}
{fileState.status !== 'UPLOADING' && (
<button onClick={() => removeFile(fileState.key)}>
Remove
</button>
)}
{/* Show error message */}
{fileState.status === 'ERROR' && (
<span style={{ color: 'red', marginLeft: '0.5rem' }}> Error: {fileState.error}</span>
)}
{/* Show link on completion */}
{fileState.status === 'COMPLETE' && fileState.url && (
<a href={fileState.url} target="_blank" rel="noopener noreferrer" style={{ color: 'green', marginLeft: '0.5rem' }}>
View File
</a>
)}
</li>
))}
</ul>
)}
</div>
);
}

5. Triggering Uploads (uploadFiles)

Call uploadFiles() to start uploading all files with status 'PENDING'. 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.

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 (
<div>
{/* ... Add files button/input and file list ... */}
{/* Upload button */}
<button
onClick={() => uploadFiles()} // Uploads all pending files
disabled={isUploading || !hasPendingFiles} // Disable if uploading or no pending files
>
{isUploading ? 'Uploading...' : 'Upload All Pending'}
</button>
</div>
);
}

6. Cancelling Uploads (cancelUpload)

Call cancelUpload(key) with the file's unique key to abort an ongoing upload. Your uploadFn must be implemented to respect the AbortSignal for cancellation to work correctly.

tsx
// Example within the file list rendering (see step 4)
{fileState.status === 'UPLOADING' && (
<button onClick={() => cancelUpload(fileState.key)}>
Cancel
</button>
)}

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, this will also attempt to cancel the upload.

tsx
// Example within the file list rendering (see step 4)
{fileState.status !== 'UPLOADING' && (
<button onClick={() => removeFile(fileState.key)}>
Remove
</button>
)}

8. Callbacks

You can pass callback props (onChange, onFileAdded, onFileRemoved, onUploadCompleted) to the UploaderProvider to execute logic when the uploader state changes.

tsx
<UploaderProvider
uploadFn={uploadFn}
onChange={({ allFiles, completedFiles }) => {
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)}
>
{/* ... */}
</UploaderProvider>

Complete Component Example (MyUploaderComponent)

Here is the MyUploaderComponent combining the steps above:

tsx
import { useUploader } from '@/components/ui/uploader'; // Adjust import path
import * as React from 'react';
function MyUploaderComponent() {
const {
fileStates,
addFiles,
removeFile,
cancelUpload,
uploadFiles,
isUploading,
} = useUploader();
const inputRef = React.useRef<HTMLInputElement>(null);
// Function to handle file selection
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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 (
<div>
{/* Hidden file input */}
<input
type="file"
ref={inputRef}
onChange={handleFileChange}
multiple // Allow multiple file selection
style={{ display: 'none' }}
/>
{/* Buttons */}
<button onClick={handleAddClick} disabled={isUploading}>Add Files</button>
<button
onClick={() => uploadFiles()}
disabled={isUploading || !hasPendingFiles}
>
{isUploading ? 'Uploading...' : 'Upload All Pending'}
</button>
{/* Display file states */}
{fileStates.length > 0 && (
<ul style={{ listStyle: 'none', padding: 0, marginTop: '1rem' }}>
{fileStates.map((fileState) => (
<li key={fileState.key} style={{ marginBottom: '0.5rem', borderBottom: '1px solid #eee', paddingBottom: '0.5rem' }}>
<span>{fileState.file.name}</span>
<span style={{ marginLeft: '0.5rem', fontSize: '0.8em', color: '#666' }}> ({fileState.status})</span>
{/* Progress and Controls */}
<div style={{ marginTop: '0.25rem' }}>
{fileState.status === 'UPLOADING' && (
<>
<progress value={fileState.progress} max="100" style={{ width: '100px', marginRight: '0.5rem' }} />
<span> {fileState.progress}%</span>
<button onClick={() => cancelUpload(fileState.key)} style={{ marginLeft: '0.5rem' }}>
Cancel
</button>
</>
)}
{fileState.status !== 'UPLOADING' && (
<button onClick={() => removeFile(fileState.key)} style={{ marginLeft: '0.5rem' }}>
Remove
</button>
)}
{fileState.status === 'ERROR' && (
<span style={{ color: 'red', marginLeft: '0.5rem' }}> Error: {fileState.error}</span>
)}
{fileState.status === 'COMPLETE' && fileState.url && (
<a href={fileState.url} target="_blank" rel="noopener noreferrer" style={{ color: 'green', marginLeft: '0.5rem' }}>
View File
</a>
)}
</div>
</li>
))}
</ul>
)}
</div>
);
}

Putting It All Together (MyUploaderPage)

Finally, use the MyUploaderComponent within the page component wrapped by the UploaderProvider.

tsx
import { UploaderProvider, UploadFn } from '@/components/ui/uploader'; // Adjust import path
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 (
<div>
<h1>My File Uploader</h1>
<UploaderProvider
uploadFn={uploadFn}
onUploadCompleted={(completedFile) => {
console.log(`File ${completedFile.file.name} uploaded successfully to ${completedFile.url}`);
// Maybe trigger a database update here
}}
>
<MyUploaderComponent />
</UploaderProvider>
</div>
);
}
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.