Client-Side Approaches
Compare the structured client vs hook-based approaches for file uploads
Client-Side Approaches
Pushduck provides two ways to integrate file uploads in your React components. Both approaches now provide identical functionality including per-route callbacks, progress tracking, and error handling.
Recommendation: Use the Enhanced Structured Client approach for the best developer experience. It now provides the same flexibility as hooks while maintaining superior type safety and centralized configuration.
Quick Comparison
🏆 Enhanced Structured Client (Recommended)
Property-based access with optional per-route configuration
const upload = createUploadClient<AppRouter>({
endpoint: '/api/upload'
})
// Simple usage
const { uploadFiles, files } = upload.imageUpload()
// With per-route callbacks (NEW!)
const { uploadFiles, files } = upload.imageUpload({
onSuccess: (results) => handleSuccess(results),
onError: (error) => handleError(error),
onProgress: (progress) => setProgress(progress)
})
Best for: Most projects - provides superior DX, type safety, and full flexibility
🪝 Hook-Based
Traditional React hook pattern
const { uploadFiles, files } = useUploadRoute<AppRouter>('imageUpload', {
onSuccess: (results) => handleSuccess(results),
onError: (error) => handleError(error),
onProgress: (progress) => setProgress(progress)
})
Best for: Teams that strongly prefer React hooks, legacy code migration
Feature Parity
Both approaches now support identical functionality:
Feature | Enhanced Structured Client | Hook-Based |
---|---|---|
✅ Type Safety | Superior - Property-based | Good - Generic types |
✅ Per-route Callbacks | ✅ Full support | ✅ Full support |
✅ Progress Tracking | ✅ Full support | ✅ Full support |
✅ Error Handling | ✅ Full support | ✅ Full support |
✅ Multiple Endpoints | ✅ Per-route endpoints | ✅ Per-route endpoints |
✅ Upload Control | ✅ Enable/disable uploads | ✅ Enable/disable uploads |
✅ Auto-upload | ✅ Per-route control | ✅ Per-route control |
✅ Overall Progress | ✅ progress, uploadSpeed, eta | ✅ progress, uploadSpeed, eta |
API Comparison: Identical Capabilities
Both approaches now return exactly the same properties and accept exactly the same configuration options:
// Hook-Based Approach
const {
uploadFiles, // (files: File[]) => Promise<void>
files, // S3UploadedFile[]
isUploading, // boolean
errors, // string[]
reset, // () => void
progress, // number (0-100) - overall progress
uploadSpeed, // number (bytes/sec) - overall speed
eta // number (seconds) - overall ETA
} = useUploadRoute<AppRouter>('imageUpload', {
onSuccess: (results) => handleSuccess(results),
onError: (error) => handleError(error),
onProgress: (progress) => setProgress(progress),
endpoint: '/api/custom-upload',
disabled: false,
autoUpload: true
});
// Enhanced Structured Client - IDENTICAL capabilities
const {
uploadFiles, // (files: File[]) => Promise<void>
files, // S3UploadedFile[]
isUploading, // boolean
errors, // string[]
reset, // () => void
progress, // number (0-100) - overall progress
uploadSpeed, // number (bytes/sec) - overall speed
eta // number (seconds) - overall ETA
} = upload.imageUpload({
onSuccess: (results) => handleSuccess(results),
onError: (error) => handleError(error),
onProgress: (progress) => setProgress(progress),
endpoint: '/api/custom-upload',
disabled: false,
autoUpload: true
});
Complete Options Parity
Both approaches support identical configuration options:
interface CommonUploadOptions {
onSuccess?: (results: UploadResult[]) => void;
onError?: (error: Error) => void;
onProgress?: (progress: number) => void;
endpoint?: string; // Custom endpoint per route
disabled?: boolean; // Enable/disable uploads
autoUpload?: boolean; // Auto-upload when files selected
}
// Hook-based: useUploadRoute(routeName, options)
// Structured: upload.routeName(options)
// Both accept the same CommonUploadOptions interface
Return Value Parity
Both approaches return identical properties:
interface CommonUploadReturn {
uploadFiles: (files: File[]) => Promise<void>;
files: S3UploadedFile[];
isUploading: boolean;
errors: string[];
reset: () => void;
// Overall progress tracking (NEW in both!)
progress?: number; // 0-100 percentage across all files
uploadSpeed?: number; // bytes per second across all files
eta?: number; // seconds remaining for all files
}
Enhanced Structured Client Examples
Basic Usage (Unchanged)
import { createUploadClient } from 'pushduck/client'
import type { AppRouter } from '@/lib/upload'
const upload = createUploadClient<AppRouter>({ endpoint: '/api/upload' })
export function SimpleUpload() {
const { uploadFiles, files, isUploading } = upload.imageUpload()
return (
<input
type="file"
onChange={(e) => uploadFiles(Array.from(e.target.files || []))}
disabled={isUploading}
/>
)
}
With Per-Route Configuration (NEW!)
export function AdvancedUpload() {
const [progress, setProgress] = useState(0)
const { uploadFiles, files, isUploading, errors, reset } =
upload.imageUpload({
onSuccess: (results) => {
console.log('✅ Upload successful!', results)
showNotification('Images uploaded successfully!')
},
onError: (error) => {
console.error('❌ Upload failed:', error)
showErrorNotification(error.message)
},
onProgress: (progress) => {
console.log(`📊 Progress: ${progress}%`)
setProgress(progress)
}
})
return (
<div>
<input type="file" onChange={(e) => uploadFiles(Array.from(e.target.files || []))} />
{progress > 0 && <progress value={progress} max={100} />}
<button onClick={reset}>Reset</button>
</div>
)
}
Multiple Routes with Different Configurations
export function MultiUploadComponent() {
// Images with progress tracking
const images = upload.imageUpload({
onProgress: (progress) => setImageProgress(progress)
})
// Documents with different endpoint and success handler
const documents = upload.documentUpload({
endpoint: '/api/secure-upload',
onSuccess: (results) => updateDocumentLibrary(results)
})
// Videos with upload disabled (feature flag)
const videos = upload.videoUpload({
disabled: !isVideoUploadEnabled
})
return (
<div>
<FileUploadSection {...images} accept="image/*" />
<FileUploadSection {...documents} accept=".pdf,.doc" />
<FileUploadSection {...videos} accept="video/*" />
</div>
)
}
Global Configuration with Per-Route Overrides
const upload = createUploadClient<AppRouter>({
endpoint: '/api/upload',
// Global defaults (optional)
defaultOptions: {
onProgress: (progress) => console.log(`Global progress: ${progress}%`),
onError: (error) => logError(error)
}
})
// This route inherits global defaults
const basic = upload.imageUpload()
// This route overrides specific options
const custom = upload.documentUpload({
endpoint: '/api/secure-upload', // Override endpoint
onSuccess: (results) => handleSecureUpload(results) // Add success handler
// Still inherits global onProgress and onError
})
Hook-Based Approach (Unchanged)
import { useUploadRoute } from 'pushduck/client'
export function HookBasedUpload() {
const { uploadFiles, files, isUploading, error } = useUploadRoute<AppRouter>('imageUpload', {
onSuccess: (results) => console.log('Success:', results),
onError: (error) => console.error('Error:', error),
onProgress: (progress) => console.log('Progress:', progress)
})
return (
<input
type="file"
onChange={(e) => uploadFiles(Array.from(e.target.files || []))}
disabled={isUploading}
/>
)
}
Migration Guide
From Hook-Based to Enhanced Structured Client
// Before: Hook-based
const { uploadFiles, files } = useUploadRoute<AppRouter>('imageUpload', {
onSuccess: handleSuccess,
onError: handleError
})
// After: Enhanced structured client
const upload = createUploadClient<AppRouter>({ endpoint: '/api/upload' })
const { uploadFiles, files } = upload.imageUpload({
onSuccess: handleSuccess,
onError: handleError
})
Benefits of Migration
- Better Type Safety: Route names are validated at compile time
- Enhanced IntelliSense: Auto-completion for all available routes
- Centralized Configuration: Single place to configure endpoints and defaults
- Refactoring Support: Rename routes safely across your codebase
- No Performance Impact: Same underlying implementation
When to Use Each Approach
Use Enhanced Structured Client When:
- ✅ Starting a new project - best overall developer experience
- ✅ Want superior type safety - compile-time route validation
- ✅ Need centralized configuration - single place for settings
- ✅ Value refactoring support - safe route renames
Use Hook-Based When:
- ✅ Migrating existing code - minimal changes required
- ✅ Dynamic route names - routes determined at runtime
- ✅ Team strongly prefers hooks - familiar React patterns
- ✅ Legacy compatibility - maintaining older codebases
Performance Considerations
Both approaches have identical performance characteristics:
- Same underlying
useUploadRoute
implementation - Same network requests and upload logic
- Same React hooks rules and lifecycle
The enhanced structured client adds zero runtime overhead while providing compile-time benefits.
Full Feature Parity: Both approaches now support the same functionality. The choice comes down to developer experience preferences rather than feature limitations.
Detailed Comparison
Type Safety & Developer Experience
// ✅ Complete type inference from server router
const upload = createUploadClient<AppRouter>({
endpoint: '/api/upload'
})
// ✅ Property-based access - no string literals
const { uploadFiles, files } = upload.imageUpload()
// ✅ IntelliSense shows all available endpoints
upload. // <- Shows: imageUpload, documentUpload, videoUpload...
// ✅ Compile-time validation
upload.nonExistentRoute() // ❌ TypeScript error
// ✅ Refactoring safety
// Rename routes in router → TypeScript shows all usage locations
Benefits:
- 🎯 Full type inference from server to client
- 🔍 IntelliSense support - discover endpoints through IDE
- 🛡️ Refactoring safety - rename with confidence
- 🚫 No string literals - eliminates typos
- ⚡ Better DX - property-based access feels natural
// ✅ With type parameter - recommended for better type safety
const { uploadFiles, files } = useUploadRoute<AppRouter>('imageUpload')
// ✅ Without type parameter - also works
const { uploadFiles, files } = useUploadRoute('imageUpload')
// Type parameter provides compile-time validation
const typed = useUploadRoute<AppRouter>('imageUpload') // Route validated
const untyped = useUploadRoute('imageUpload') // Any string accepted
Characteristics:
- 🪝 React hook pattern - familiar to React developers
- 🔤 Flexible usage - works with or without type parameter
- 🧩 Component-level state - each hook manages its own state
- 🎯 Type safety - enhanced when using
<AppRouter>
- 🔍 IDE support - best with type parameter
Code Examples
Structured Client:
import { upload } from '@/lib/upload-client'
export function ImageUploader() {
const { uploadFiles, files, isUploading, error } = upload.imageUpload()
return (
<div>
<input
type="file"
onChange={(e) => uploadFiles(Array.from(e.target.files || []))}
disabled={isUploading}
/>
{/* Upload UI */}
</div>
)
}
Hook-Based:
import { useUploadRoute } from 'pushduck/client'
export function ImageUploader() {
const { uploadFiles, files, isUploading, error } = useUploadRoute<AppRouter>('imageUpload')
return (
<div>
<input
type="file"
onChange={(e) => uploadFiles(Array.from(e.target.files || []))}
disabled={isUploading}
/>
{/* Same upload UI */}
</div>
)
}
Structured Client:
export function FileManager() {
const images = upload.imageUpload()
const documents = upload.documentUpload()
const videos = upload.videoUpload()
return (
<div>
<ImageSection {...images} />
<DocumentSection {...documents} />
<VideoSection {...videos} />
</div>
)
}
Hook-Based:
export function FileManager() {
const images = useUploadRoute<AppRouter>('imageUpload')
const documents = useUploadRoute<AppRouter>('documentUpload')
const videos = useUploadRoute<AppRouter>('videoUpload')
return (
<div>
<ImageSection {...images} />
<DocumentSection {...documents} />
<VideoSection {...videos} />
</div>
)
}
Structured Client:
// lib/upload-client.ts
export const upload = createUploadClient<AppRouter>({
endpoint: '/api/upload',
headers: {
Authorization: `Bearer ${getAuthToken()}`
}
})
// components/secure-uploader.tsx
export function SecureUploader() {
const { uploadFiles } = upload.secureUpload()
// Authentication handled globally
}
Hook-Based:
export function SecureUploader() {
const { uploadFiles } = useUploadRoute<AppRouter>('secureUpload', {
headers: {
Authorization: `Bearer ${getAuthToken()}`
}
})
// Authentication per hook usage
}
Conclusion
Our Recommendation: Use the Enhanced Structured Client approach (createUploadClient
) for most projects. It provides superior developer experience, better refactoring safety, and enhanced type inference.
Both approaches are supported: The hook-based approach (useUploadRoute<AppRouter>
) is fully supported and valid for teams that prefer traditional React patterns.
Quick Decision Guide:
- Most projects → Use
createUploadClient
(recommended) - Strongly prefer React hooks → Use
useUploadRoute<AppRouter>
- Want best DX and type safety → Use
createUploadClient
- Need component-level control → Use
useUploadRoute<AppRouter>
Next Steps
- New Project: Start with createUploadClient
- Existing Hook Code: Consider migrating gradually
- Need Help: Join our Discord community for guidance