# 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 (
{/* Hidden file input */} {/* Buttons */} {/* Display file states */} {fileStates.length > 0 && (
    {fileStates.map((fileState) => (
  • {fileState.file.name} {' '} ({fileState.status}) {/* Progress and Controls */}
    {fileState.status === 'UPLOADING' && ( <> {fileState.progress}% )} {fileState.status !== 'UPLOADING' && ( )} {fileState.status === 'ERROR' && ( {' '} Error: {fileState.error} )} {fileState.status === 'COMPLETE' && fileState.url && ( View File )}
  • ))}
)}
); } ``` ### 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.