Guides/Migration

Enhanced Client Migration

Migrate from hook-based API to property-based client access for better type safety

Enhanced Client Migration

Upgrade to the new property-based client API for enhanced type safety, better developer experience, and elimination of string literals.

The enhanced client API is 100% backward compatible. You can migrate gradually without breaking existing code.

Why Migrate?

Enhanced Type Safety

Complete type inference from server to client

// ❌ Old: String literals, no type safety 
const {uploadFiles} = useUploadRoute("imageUpload") 
// ✅ New: Property-based, full type inference 
const {uploadFiles} = upload.imageUpload 

Better Developer Experience

IntelliSense shows all available endpoints

// ✅ Autocomplete shows all your endpoints upload. 
// imageUpload, documentUpload, videoUpload... 
// ^ No more guessing endpoint names 

Refactoring Safety

Rename endpoints safely across your codebase

 // When you rename routes in your router, 
 // TypeScript shows errors everywhere they're used 
 // Making refactoring safe and easy 

Migration Steps

Install Latest Version

Ensure you're using the latest version of pushduck:

npm install pushduck@latest
yarn add pushduck@latest
pnpm add pushduck@latest
bun add pushduck@latest

Create Upload Client

Set up your typed upload client:

lib/upload-client.ts
import { createUploadClient } from 'pushduck/client'
import type { AppRouter } from './upload' // Your router type

export const upload = createUploadClient<AppRouter>({
  endpoint: '/api/upload'
})

Migrate Components Gradually

Update your components one by one:

import { useUploadRoute } from 'pushduck/client'

export function ImageUploader() {
  const { uploadFiles, files, isUploading } = useUploadRoute('imageUpload')
  
  return (
    <div>
      <input type="file" onChange={(e) => uploadFiles(e.target.files)} />
      {/* Upload UI */}
    </div>
  )
}
import { upload } from '@/lib/upload-client'

export function ImageUploader() {
  const { uploadFiles, files, isUploading } = upload.imageUpload
  
  return (
    <div>
      <input type="file" onChange={(e) => uploadFiles(e.target.files)} />
      {/* Same upload UI */}
    </div>
  )
}

Update Imports

Once migrated, you can remove old hook imports:

// Remove old imports
// import { useUploadRoute } from 'pushduck/client'

// Use new client import
import { upload } from '@/lib/upload-client'

Migration Examples

Basic Component Migration

import { useUploadRoute } from 'pushduck/client'

export function DocumentUploader() {
  const { 
    uploadFiles, 
    files, 
    isUploading, 
    error, 
    reset 
  } = useUploadRoute('documentUpload', {
    onSuccess: (results) => {
      console.log('Uploaded:', results)
    },
    onError: (error) => {
      console.error('Error:', error)
    }
  })
  
  return (
    <div>
      <input 
        type="file" 
        onChange={(e) => uploadFiles(Array.from(e.target.files || []))}
        disabled={isUploading}
      />
      
      {files.map(file => (
        <div key={file.id}>
          <span>{file.name}</span>
          <progress value={file.progress} max={100} />
        </div>
      ))}
      
      {error && <div>Error: {error.message}</div>}
      <button onClick={reset}>Reset</button>
    </div>
  )
}
import { upload } from '@/lib/upload-client'

export function DocumentUploader() {
  const { 
    uploadFiles, 
    files, 
    isUploading, 
    error, 
    reset 
  } = upload.documentUpload
  
  // Handle callbacks with upload options
  const handleUpload = async (selectedFiles: File[]) => {
    try {
      const results = await uploadFiles(selectedFiles)
      console.log('Uploaded:', results)
    } catch (error) {
      console.error('Error:', error)
    }
  }
  
  return (
    <div>
      <input 
        type="file" 
        onChange={(e) => handleUpload(Array.from(e.target.files || []))}
        disabled={isUploading}
      />
      
      {files.map(file => (
        <div key={file.id}>
          <span>{file.name}</span>
          <progress value={file.progress} max={100} />
        </div>
      ))}
      
      {error && <div>Error: {error.message}</div>}
      <button onClick={reset}>Reset</button>
    </div>
  )
}

Form Integration Migration

import { useForm } from 'react-hook-form'
import { useUploadRoute } from 'pushduck/client'

export function ProductForm() {
  const { register, handleSubmit, setValue } = useForm()
  
  const { uploadFiles, uploadedFiles } = useUploadRoute('productImages', {
    onSuccess: (results) => {
      setValue('images', results.map(r => r.url))
    }
  })
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('name')} placeholder="Product name" />
      <input 
        type="file" 
        multiple 
        onChange={(e) => uploadFiles(Array.from(e.target.files || []))}
      />
      <button type="submit">Save Product</button>
    </form>
  )
}
import { useForm } from 'react-hook-form'
import { upload } from '@/lib/upload-client'

export function ProductForm() {
  const { register, handleSubmit, setValue } = useForm()
  const { uploadFiles } = upload.productImages
  
  const handleImageUpload = async (files: File[]) => {
    const results = await uploadFiles(files)
    setValue('images', results.map(r => r.url))
  }
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('name')} placeholder="Product name" />
      <input 
        type="file" 
        multiple 
        onChange={(e) => handleImageUpload(Array.from(e.target.files || []))}
      />
      <button type="submit">Save Product</button>
    </form>
  )
}

Multiple Upload Types Migration

export function MediaUploader() {
  const images = useUploadRoute('imageUpload')
  const videos = useUploadRoute('videoUpload')
  const documents = useUploadRoute('documentUpload')
  
  return (
    <div>
      <div>
        <h3>Images</h3>
        <input type="file" onChange={(e) => images.uploadFiles(e.target.files)} />
      </div>
      <div>
        <h3>Videos</h3>
        <input type="file" onChange={(e) => videos.uploadFiles(e.target.files)} />
      </div>
      <div>
        <h3>Documents</h3>
        <input type="file" onChange={(e) => documents.uploadFiles(e.target.files)} />
      </div>
    </div>
  )
}
import { upload } from '@/lib/upload-client'

export function MediaUploader() {
  const images = upload.imageUpload
  const videos = upload.videoUpload
  const documents = upload.documentUpload
  
  return (
    <div>
      <div>
        <h3>Images</h3>
        <input type="file" onChange={(e) => images.uploadFiles(e.target.files)} />
      </div>
      <div>
        <h3>Videos</h3>
        <input type="file" onChange={(e) => videos.uploadFiles(e.target.files)} />
      </div>
      <div>
        <h3>Documents</h3>
        <input type="file" onChange={(e) => documents.uploadFiles(e.target.files)} />
      </div>
    </div>
  )
}

Key Differences

API Comparison

FeatureHook-Based APIProperty-Based API
Type SafetyRuntime string validationCompile-time type checking
IntelliSenseLimited autocompleteFull endpoint autocomplete
RefactoringManual find/replaceAutomatic TypeScript errors
Bundle SizeSlightly largerOptimized tree-shaking
Learning CurveFamiliar React patternNew property-based pattern

Callback Handling

const { uploadFiles } = useUploadRoute('images', {
  onSuccess: (results) => console.log('Success:', results),
  onError: (error) => console.error('Error:', error),
  onProgress: (progress) => console.log('Progress:', progress)
})
const { uploadFiles } = upload.images

await uploadFiles(files, {
  onSuccess: (results) => console.log('Success:', results),
  onError: (error) => console.error('Error:', error),
  onProgress: (progress) => console.log('Progress:', progress)
})

Troubleshooting

Common Migration Issues

Type Errors: If you see TypeScript errors after migration, ensure your router type is properly exported and imported.

// ❌ Missing router type
export const upload = createUploadClient({
  endpoint: "/api/upload",
});

// ✅ With proper typing
export const upload = createUploadClient<AppRouter>({
  endpoint: "/api/upload",
});

Gradual Migration Strategy

You can use both APIs simultaneously during migration:

// Keep existing hook-based components working
const hookUpload = useUploadRoute("imageUpload");

// Use new property-based API for new components
const propertyUpload = upload.imageUpload;

// Both work with the same backend!

Benefits After Migration

  • 🎯 Enhanced Type Safety: Catch errors at compile time, not runtime
  • 🚀 Better Performance: Optimized bundle size with tree-shaking
  • 💡 Improved DX: Full IntelliSense support for all endpoints
  • 🔧 Safe Refactoring: Rename endpoints without breaking your app
  • 📦 Future-Proof: Built for the next generation of pushduck features

Migration Complete! You now have enhanced type safety and a better developer experience. Need help? Join our Discord community for support.