Skip to main content

useUploader

WIP

The 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)

PropertyTypeDescription
itemsEngine.Item[]All items, in insertion order
byPhaseRecord<Engine.Phase, Engine.Item[]>Items grouped by phase
uploadingEngine.Item[]Items in uploading
pausedEngine.Item[]Items in paused
completedEngine.Item[]Items in completed
failedEngine.Item[]Items in error (exposed as failed for convenience)
readyEngine.Item[]Items in ready
dispatch(cmd: Engine.Command<P>) => voidSend commands
on / offevent helpersSubscribe / 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.