useUploadRoute
React hook for uploading files with reactive state management
useUploadRoute Hook
A React hook that provides reactive state management for file uploads with progress tracking and error handling.
This hook follows familiar React patterns and is perfect for teams that prefer
the traditional hook-based approach. Both this and createUploadClient
are equally valid ways to handle uploads.
When to Use This Hook
Use useUploadRoute
when:
- 🪝 Prefer React hooks - familiar pattern for React developers
- 🧩 Granular control needed over individual upload state
- 🔄 Component-level state management preferred
- 👥 Team preference for hook-based patterns
Alternative Approach
You can also use the structured client approach:
// Hook-based approach
import { useUploadRoute } from 'pushduck/client'
const { uploadFiles, files } = useUploadRoute<AppRouter>('imageUpload')
// Structured client approach
import { createUploadClient } from 'pushduck/client'
const upload = createUploadClient<AppRouter>({
endpoint: '/api/upload'
})
const { uploadFiles, files } = upload.imageUpload()
Both approaches provide the same functionality and type safety - choose what feels more natural for your team.
Basic Usage
import { useUploadRoute } from "pushduck/client";
import { formatETA, formatUploadSpeed } from "pushduck";
import type { AppRouter } from "@/lib/upload";
export function ImageUploader() {
// With type parameter (recommended for better type safety)
const { uploadFiles, files, isUploading, error, reset, progress, uploadSpeed, eta } =
useUploadRoute<AppRouter>("imageUpload");
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFiles = Array.from(e.target.files || []);
uploadFiles(selectedFiles);
};
return (
<div>
<input
type="file"
multiple
accept="image/*"
onChange={handleFileSelect}
/>
{/* Overall Progress Tracking */}
{isUploading && files.length > 1 && progress !== undefined && (
<div className="overall-progress">
<h3>Overall Progress: {Math.round(progress)}%</h3>
<progress value={progress} max={100} />
<div>
<span>Speed: {uploadSpeed ? formatUploadSpeed(uploadSpeed) : '0 B/s'}</span>
<span>ETA: {eta ? formatETA(eta) : '--'}</span>
</div>
</div>
)}
{/* Individual File Progress */}
{files.map((file) => (
<div key={file.id}>
<span>{file.name}</span>
<progress value={file.progress} max={100} />
{file.status === "success" && <a href={file.url}>View</a>}
</div>
))}
{error && <div className="error">{error.message}</div>}
<button onClick={reset}>Reset</button>
</div>
);
}
Overall Progress Tracking
The hook provides real-time overall progress metrics when uploading multiple files:
Overall progress tracking is especially useful for batch uploads and provides a better user experience when uploading multiple files simultaneously.
const { progress, uploadSpeed, eta } = useUploadRoute("imageUpload");
// Progress: 0-100 percentage across all files
console.log(`Overall progress: ${progress}%`);
// Upload speed: Combined transfer rate in bytes/second
console.log(`Transfer rate: ${formatUploadSpeed(uploadSpeed)}`);
// ETA: Time remaining in seconds
console.log(`Time remaining: ${formatETA(eta)}`);
Progress Calculation
- progress: Weighted by file sizes, not just file count
- uploadSpeed: Sum of all active file upload speeds
- eta: Calculated based on remaining bytes and current speed
- Values are
undefined
when no uploads are active
Hook Signature
// With type parameter (recommended)
function useUploadRoute<TRouter>(
route: keyof TRouter,
options?: UseUploadOptions
): UseUploadReturn;
// Without type parameter (also works)
function useUploadRoute(
route: string,
options?: UseUploadOptions
): UseUploadReturn;
Parameters
Prop | Type | Default |
---|---|---|
route | keyof TRouter | string | - |
options? | UseUploadOptions | - |
Type Parameter Benefits
// ✅ With type parameter - better type safety
const { uploadFiles } = useUploadRoute<AppRouter>("imageUpload");
// - Route names are validated at compile time
// - IntelliSense shows available routes
// - Typos caught during development
// ✅ Without type parameter - still works
const { uploadFiles } = useUploadRoute("imageUpload");
// - Works with any string
// - Less type safety but more flexible
// - Good for dynamic route names
Options
Prop | Type | Default |
---|---|---|
onStart? | (files: S3FileMetadata[]) => void | - |
onSuccess? | (results: UploadResult[]) => void | - |
onError? | (error: UploadError) => void | - |
onProgress? | (progress: number) => void | - |
Return Value
Prop | Type | Default |
---|---|---|
uploadFiles? | (files: File[]) => Promise<UploadResult[]> | - |
files? | UploadFile[] | - |
isUploading? | boolean | - |
uploadedFiles? | UploadResult[] | - |
error? | UploadError | null | - |
reset? | () => void | - |
progress? | number | undefined | - |
uploadSpeed? | number | undefined | - |
eta? | number | undefined | - |
Callback Execution Order
The callbacks follow a predictable order to provide clear upload lifecycle management:
Proper Callback Sequence: onStart
→ onProgress(0)
→ onProgress(n)
→ onSuccess/onError
const { uploadFiles } = useUploadRoute<AppRouter>('imageUpload', {
// 1. Called first after validation passes
onStart: (files) => {
console.log('🚀 Upload starting for', files.length, 'files');
setUploading(true);
},
// 2. Called with progress updates (0-100)
onProgress: (progress) => {
console.log('📊 Progress:', progress + '%');
setProgress(progress);
},
// 3. Called on completion
onSuccess: (results) => {
console.log('✅ Upload complete!');
results.forEach(file => {
console.log('File URL:', file.url); // Permanent URL
console.log('Download URL:', file.presignedUrl); // Temporary access (1 hour)
});
setUploading(false);
},
// OR 3. Called on error (no progress callbacks for validation errors)
onError: (error) => {
console.log('❌ Upload failed:', error.message);
setUploading(false);
}
});
Validation Errors vs Upload Errors
- Validation errors (size limits, file types): Only
onError
is called - Upload errors (network issues):
onStart
→onProgress(0)
→onError
Upload Result Structure
Each successfully uploaded file includes the following properties:
Prop | Type | Default |
---|---|---|
id? | string | - |
name? | string | - |
size? | number | - |
type? | string | - |
status? | "pending" | "uploading" | "success" | "error" | - |
progress? | number | - |
url? | string | undefined | - |
key? | string | undefined | - |
presignedUrl? | string | undefined | - |
error? | string | undefined | - |
file? | File | undefined | - |
uploadStartTime? | number | undefined | - |
uploadSpeed? | number | undefined | - |
eta? | number | undefined | - |
URL Usage Examples
const { uploadFiles } = useUploadRoute('fileUpload', {
onSuccess: (results) => {
results.forEach(file => {
// Use permanent URL for public files
if (file.url) {
console.log('Public URL:', file.url);
}
// Use presigned URL for private files or temporary access
if (file.presignedUrl) {
console.log('Download URL:', file.presignedUrl);
// This URL expires in 1 hour and can be used for secure downloads
}
});
}
});
Advanced Examples
const { uploadFiles, files } = useUploadRoute<AppRouter>('documentUpload', {
onStart: (files) => {
toast.info(`Starting upload of ${files.length} files...`);
setUploadStarted(true);
},
onSuccess: (results) => {
toast.success(`Uploaded ${results.length} files`);
// Store both permanent and temporary URLs
updateDocuments(results.map(file => ({
url: file.url, // Permanent access
downloadUrl: file.presignedUrl, // Temporary access (1 hour)
name: file.name,
key: file.key
})));
setUploadStarted(false);
},
onError: (error) => {
toast.error(`Upload failed: ${error.message}`);
setUploadStarted(false);
},
onProgress: (progress) => {
setGlobalProgress(progress);
}
})
const images = useUploadRoute<AppRouter>('imageUpload')
const documents = useUploadRoute<AppRouter>('documentUpload')
return (
<div>
<FileUploadSection {...images} accept="image/*" />
<FileUploadSection {...documents} accept=".pdf,.doc" />
</div>
)
const { uploadFiles, uploadedFiles } = useUploadRoute<AppRouter>('attachments', {
onSuccess: (results) => {
setValue('attachments', results.map(r => r.url))
}
})
const onSubmit = (data) => {
// Form data includes uploaded file URLs
console.log(data.attachments)
}
Flexible API: Use this hook when you prefer React's familiar hook patterns or need more granular control over upload state.
createUploadClient
Create a type-safe upload client with property-based access and optional per-route configuration
Troubleshooting
Fix common pushduck issues including CORS errors, TypeScript problems, upload failures, and environment variable configuration. Complete troubleshooting guide with solutions.