Guides
Step-by-step guides for common upload scenarios.
Quick Start Guide
This guide walks through setting up a basic file upload from scratch.
Step 1: Set Up the Store
import { createUploadStore } from '@gentleduck/upload/core'
import { createStrategyRegistry, PostStrategy } from '@gentleduck/upload/strategies'
// Register the POST strategy for simple uploads
const strategies = createStrategyRegistry()
strategies.set(PostStrategy())
// Define your backend adapter
const api = {
createIntent: async ({ file, purpose }) => {
const res = await fetch('/api/uploads/intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fileName: file.name, size: file.size, purpose }),
})
return res.json()
},
complete: async ({ fileId }) => {
const res = await fetch(`/api/uploads/${fileId}/complete`, { method: 'POST' })
return res.json()
},
}
const store = createUploadStore({ api, strategies })import { createUploadStore } from '@gentleduck/upload/core'
import { createStrategyRegistry, PostStrategy } from '@gentleduck/upload/strategies'
// Register the POST strategy for simple uploads
const strategies = createStrategyRegistry()
strategies.set(PostStrategy())
// Define your backend adapter
const api = {
createIntent: async ({ file, purpose }) => {
const res = await fetch('/api/uploads/intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fileName: file.name, size: file.size, purpose }),
})
return res.json()
},
complete: async ({ fileId }) => {
const res = await fetch(`/api/uploads/${fileId}/complete`, { method: 'POST' })
return res.json()
},
}
const store = createUploadStore({ api, strategies })Step 2: Add Files
// From a file input or drag-and-drop
store.dispatch({
type: 'addFiles',
files: selectedFiles,
purpose: 'document',
})// From a file input or drag-and-drop
store.dispatch({
type: 'addFiles',
files: selectedFiles,
purpose: 'document',
})Step 3: Start Uploads
If autoStart is not configured, start uploads manually:
store.dispatch({ type: 'start', localId: 'some-local-id' })store.dispatch({ type: 'start', localId: 'some-local-id' })Step 4: Listen for Completion
store.on('upload.completed', ({ localId, result }) => {
console.log(`Upload ${localId} completed:`, result)
})store.on('upload.completed', ({ localId, result }) => {
console.log(`Upload ${localId} completed:`, result)
})React Quick Start
Wrap Your App
import { UploadProvider } from '@gentleduck/upload/react'
function App() {
return (
<UploadProvider store={store}>
<UploadPage />
</UploadProvider>
)
}import { UploadProvider } from '@gentleduck/upload/react'
function App() {
return (
<UploadProvider store={store}>
<UploadPage />
</UploadProvider>
)
}Build an Upload Component
import { useUploader } from '@gentleduck/upload/react'
import React from 'react'
function UploadPage() {
const { items, dispatch, on } = useUploader()
React.useEffect(() => {
return on('upload.completed', ({ localId, result }) => {
console.log('Done:', localId, result)
})
}, [on])
const handleFiles = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files ?? [])
dispatch({ type: 'addFiles', files, purpose: 'avatar' })
}
return (
<div>
<input type="file" multiple onChange={handleFiles} />
<ul>
{items.map((item) => (
<li key={item.localId}>
{item.file?.name} — {item.phase}
{item.phase === 'uploading' && ` (${Math.round(item.progress ?? 0)}%)`}
</li>
))}
</ul>
</div>
)
}import { useUploader } from '@gentleduck/upload/react'
import React from 'react'
function UploadPage() {
const { items, dispatch, on } = useUploader()
React.useEffect(() => {
return on('upload.completed', ({ localId, result }) => {
console.log('Done:', localId, result)
})
}, [on])
const handleFiles = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files ?? [])
dispatch({ type: 'addFiles', files, purpose: 'avatar' })
}
return (
<div>
<input type="file" multiple onChange={handleFiles} />
<ul>
{items.map((item) => (
<li key={item.localId}>
{item.file?.name} — {item.phase}
{item.phase === 'uploading' && ` (${Math.round(item.progress ?? 0)}%)`}
</li>
))}
</ul>
</div>
)
}Large File Uploads with Multipart
For files that need resumability, use the multipart strategy:
import { createStrategyRegistry, PostStrategy, multipartStrategy } from '@gentleduck/upload/strategies'
const strategies = createStrategyRegistry()
strategies.set(PostStrategy())
strategies.set(multipartStrategy())import { createStrategyRegistry, PostStrategy, multipartStrategy } from '@gentleduck/upload/strategies'
const strategies = createStrategyRegistry()
strategies.set(PostStrategy())
strategies.set(multipartStrategy())Your backend's createIntent should return a multipart intent for large files:
const api = {
createIntent: async ({ file, purpose }) => {
const strategy = file.size > 10 * 1024 * 1024 ? 'multipart' : 'post'
// Call your backend which returns the correct intent shape
const res = await fetch('/api/uploads/intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fileName: file.name, size: file.size, purpose, strategy }),
})
return res.json()
},
complete: async ({ fileId }) => {
const res = await fetch(`/api/uploads/${fileId}/complete`, { method: 'POST' })
return res.json()
},
}const api = {
createIntent: async ({ file, purpose }) => {
const strategy = file.size > 10 * 1024 * 1024 ? 'multipart' : 'post'
// Call your backend which returns the correct intent shape
const res = await fetch('/api/uploads/intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fileName: file.name, size: file.size, purpose, strategy }),
})
return res.json()
},
complete: async ({ fileId }) => {
const res = await fetch(`/api/uploads/${fileId}/complete`, { method: 'POST' })
return res.json()
},
}The engine automatically selects the correct strategy based on the strategy field in the returned intent.
Enabling Persistence
To allow uploads to resume after a page refresh:
import { LocalStorageAdapter } from '@gentleduck/upload/core'
const store = createUploadStore({
api,
strategies,
persistence: {
key: 'uploads',
version: 1,
adapter: LocalStorageAdapter,
isPurpose: (value) => value === 'avatar' || value === 'document',
isIntent: (value) =>
typeof value === 'object' && value !== null && 'strategy' in value && 'fileId' in value,
},
})import { LocalStorageAdapter } from '@gentleduck/upload/core'
const store = createUploadStore({
api,
strategies,
persistence: {
key: 'uploads',
version: 1,
adapter: LocalStorageAdapter,
isPurpose: (value) => value === 'avatar' || value === 'document',
isIntent: (value) =>
typeof value === 'object' && value !== null && 'strategy' in value && 'fileId' in value,
},
})Restored items come back in a paused state without a file reference. Use the rebind command to attach the file before resuming.