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

PropTypeDefault
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

PropTypeDefault
onStart?
(files: S3FileMetadata[]) => void
-
onSuccess?
(results: UploadResult[]) => void
-
onError?
(error: UploadError) => void
-
onProgress?
(progress: number) => void
-

Return Value

PropTypeDefault
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: onStartonProgress(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): onStartonProgress(0)onError

Upload Result Structure

Each successfully uploaded file includes the following properties:

PropTypeDefault
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.