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.
If you are installing the other dropzone components via the CLI, this component will be installed automatically. You can skip the following steps.
Installation
npx 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 thefile
, anonProgressChange
callback, and anAbortSignal
. It should return an object with the uploaded file'surl
.autoUpload
: (Optional, default:false
) Iftrue
, files will start uploading immediately after being added.
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.
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
.
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 originalFile
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.
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.
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.
// 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.
// 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.
<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:
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
.
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.