Guides
WIPCopy-paste recipes for common upload scenarios.
Focused recipes built on the real API. Each assumes the shared types from Installation.
Minimal store (vanilla)
import type { Contracts } from '@gentleduck/upload/core'
import { createUploadStore } from '@gentleduck/upload/core'
import { PostStrategy, createStrategyRegistry } from '@gentleduck/upload/strategies'
type Intents = { post: PostStrategy.Intent }
type Cursors = { post?: PostStrategy.Cursor }
type Purpose = 'document'
type Result = Contracts.Result.Base & { url: string }
const api: Contracts.Api.Me<Intents, Purpose, Result> = {
async createIntent({ purpose, contentType, size, filename }, ctx) {
const res = await fetch('/api/uploads/intent', {
method: 'POST',
body: JSON.stringify({ purpose, contentType, size, filename }),
signal: ctx.signal,
})
return res.json()
},
async complete({ fileId, filename, contentType, size }, ctx) {
const res = await fetch(`/api/uploads/${fileId}/complete`, {
method: 'POST',
body: JSON.stringify({ filename, contentType, size }),
signal: ctx.signal,
})
return res.json()
},
}
const strategies = createStrategyRegistry<Intents, Cursors, Purpose, Result>()
strategies.set(PostStrategy())
export const store = createUploadStore<Intents, Cursors, Purpose, Result>({ api, strategies })
Drive it:
store.dispatch({ type: 'addFiles', files: selectedFiles, purpose: 'document' })
store.dispatch({ type: 'startAll' })
store.on('upload.completed', ({ localId, result }) => console.log(localId, result))
Without autoStart, start manually with start/startAll.
React quick start
import { UploadProvider, useUploader } from '@gentleduck/upload/react'
import { store } from './upload'
export function App() {
return (
<UploadProvider store={store}>
<UploadPage />
</UploadProvider>
)
}
function UploadPage() {
const { items, dispatch, on } = useUploader<Intents, Cursors, Purpose, Result>()
React.useEffect(() => on('upload.completed', ({ localId, result }) =>
console.log('done', localId, result)
), [on])
return (
<div>
<input
type="file"
multiple
onChange={(e) =>
dispatch({ type: 'addFiles', files: Array.from(e.target.files ?? []), purpose: 'document' })
}
/>
<ul>
{items.map((item) => (
<li key={item.localId}>
{item.fingerprint.name} — {item.phase}
{item.phase === 'uploading' && ` (${item.progress.pct.toFixed(0)}%)`}
</li>
))}
</ul>
</div>
)
}
item.progress only exists once you're in a phase that has it (uploading, completing,
paused) — narrow on item.phase first.
Support large files with multipart
Register both strategies and let the backend pick per file:
import { PostStrategy, multipartStrategy, MultipartStrategy, createStrategyRegistry } from '@gentleduck/upload/strategies'
type Intents = { post: PostStrategy.Intent; multipart: MultipartStrategy.Intent }
type Cursors = { post?: PostStrategy.Cursor; multipart?: MultipartStrategy.Cursor }
const strategies = createStrategyRegistry<Intents, Cursors, Purpose, Result>()
strategies.set(PostStrategy())
strategies.set(multipartStrategy({ maxPartConcurrency: 4 }))
Your createIntent chooses a strategy by returning the matching intent — the engine dispatches
on the intent's strategy field. The multipart strategy also needs api.multipart.signPart
and api.multipart.completeMultipart (see Multipart Strategy).
Auto-start selected purposes
createUploadStore({
api,
strategies,
config: { autoStart: ['avatar'] }, // or: autoStart: (p) => p !== 'draft'
})
Matching files upload as soon as their intent is created — no manual start.
Enable resume across reloads
import { IndexedDBAdapter } from '@gentleduck/upload/core'
const store = createUploadStore<Intents, Cursors, Purpose, Result>({
api,
strategies,
persistence: {
key: 'uploads',
version: 1,
adapter: IndexedDBAdapter,
isPurpose: (v): v is Purpose => v === 'avatar' || v === 'document',
isIntent: (v): v is Intents[keyof Intents] =>
typeof v === 'object' && v !== null && 'strategy' in v && 'fileId' in v,
},
})
Restored items come back paused without a File. Rebind before resuming:
store.dispatch({ type: 'rebind', localId, file: reselectedFile })
store.dispatch({ type: 'resume', localId })
See Persistence for the full flow.
Await a batch of uploads
const ids = Array.from(store.getSnapshot().items.keys())
for (const outcome of await store.waitFor(ids)) {
if (outcome.status === 'completed') console.log(outcome.result.key)
else if (outcome.status === 'error') console.warn(outcome.error.code)
}