useUploader
WIPThe React hook for reading upload state and dispatching commands.
useUploader subscribes to the store with useSyncExternalStore and returns the items, phase
buckets, and the dispatch/on/off handles. It re-renders when the snapshot changes.
const {
items, byPhase, uploading, paused, completed, failed, ready,
dispatch, on, off,
} = useUploader<Intents, Cursors, Purpose, Result>()
Pass a store explicitly to bypass context: useUploader(store). With no argument it reads the
store from UploadProvider.
Return shape (Uploader.State)
| Property | Type | Description |
|---|---|---|
items | Engine.Item[] | All items, in insertion order |
byPhase | Record<Engine.Phase, Engine.Item[]> | Items grouped by phase |
uploading | Engine.Item[] | Items in uploading |
paused | Engine.Item[] | Items in paused |
completed | Engine.Item[] | Items in completed |
failed | Engine.Item[] | Items in error (exposed as failed for convenience) |
ready | Engine.Item[] | Items in ready |
dispatch | (cmd: Engine.Command<P>) => void | Send commands |
on / off | event helpers | Subscribe / unsubscribe (on returns an unsubscribe fn) |
Note failed maps to the error phase — when you check item.phase, use 'error'.
Dispatching commands
dispatch({ type: 'addFiles', files: selectedFiles, purpose: 'avatar' })
dispatch({ type: 'start', localId })
dispatch({ type: 'pause', localId })
dispatch({ type: 'resume', localId })
dispatch({ type: 'cancel', localId })
dispatch({ type: 'retry', localId })
dispatch({ type: 'remove', localId })
Batch variants (startAll, pauseAll, cancelAll) accept an optional purpose filter.
Listening for events
on returns its own unsubscribe function — return it straight from an effect:
React.useEffect(() => on('upload.progress', ({ localId, pct }) => {
console.log(localId, pct)
}), [on])
Rendering phase-specific fields
Engine.Item is a discriminated union, so narrow on phase before touching phase-only fields:
{items.map((item) => (
<div key={item.localId}>
<span>{item.fingerprint.name}</span>
{item.phase === 'uploading' && <progress value={item.progress.pct} max={100} />}
{item.phase === 'error' && item.retryable && (
<button onClick={() => dispatch({ type: 'retry', localId: item.localId })}>Retry</button>
)}
</div>
))}
Variants
useUploaderActions
Returns { dispatch, on, store } without subscribing to snapshots — use it for buttons or
side-effect components that shouldn't re-render on progress:
function StartAllButton() {
const { dispatch } = useUploaderActions<Intents, Cursors, Purpose, Result>()
return <button onClick={() => dispatch({ type: 'startAll' })}>Start all</button>
}
createUploadFactory
Bind a useUploader to a specific store so every call is typed without repeating generics:
import { createUploadFactory } from '@gentleduck/upload/react'
import { store } from './upload'
export const useMyUploader = createUploadFactory(store)
function List() {
const { items, dispatch } = useMyUploader() // fully typed, no context needed
// …
}
Re-render notes
useUploader re-renders on every snapshot change, which includes progress. The engine throttles
progress with config.progressThrottleMs (default 100), so updates stay smooth. For zero
progress-driven re-renders, use useUploaderActions instead.