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:
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
Feature | Hook-Based API | Property-Based API |
---|---|---|
Type Safety | Runtime string validation | Compile-time type checking |
IntelliSense | Limited autocomplete | Full endpoint autocomplete |
Refactoring | Manual find/replace | Automatic TypeScript errors |
Bundle Size | Slightly larger | Optimized tree-shaking |
Learning Curve | Familiar React pattern | New 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.