Guides

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:

FeatureEnhanced Structured ClientHook-Based
✅ Type SafetySuperior - Property-basedGood - 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

  1. Better Type Safety: Route names are validated at compile time
  2. Enhanced IntelliSense: Auto-completion for all available routes
  3. Centralized Configuration: Single place to configure endpoints and defaults
  4. Refactoring Support: Rename routes safely across your codebase
  5. 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