Skip to content

React Hooks

React bindings for Durably - hooks for triggering and monitoring jobs with real-time updates.

Requirements

  • React 19+ (uses React.use() for Promise resolution)

Which Mode Should I Use?

Durably React provides two modes for different architectures:

QuestionSPA HooksFullstack Hooks
Where do jobs run?In the browserOn the server
Where is data stored?Browser OPFSServer database
Works offline?YesNo
Share state across tabs/users?NoYes
Needs backend?NoYes

Choose Fullstack Hooks when:

  • Building full-stack applications
  • Jobs need server resources (databases, APIs, secrets)
  • Multiple users or tabs need to see the same state
  • Need persistent storage across devices
tsx
import { createDurably } from '@coji/durably-react'

Fullstack Hooks Reference →

Choose SPA Hooks when:

  • Building offline-capable or local-first apps
  • Data should stay on the user's device
  • Prototyping without a backend
  • Single-user, single-tab usage
tsx
import { DurablyProvider, useJob } from '@coji/durably-react/spa'

SPA Hooks Reference →

Installation

bash
# Fullstack mode - connects to Durably server
pnpm add @coji/durably-react

# SPA mode - runs Durably in the browser
pnpm add @coji/durably @coji/durably-react kysely zod sqlocal

Quick Examples

Fullstack Mode

Jobs run on the server, with real-time updates via SSE.

tsx
// 1. Create type-safe hooks (client-side file)
import { createDurably } from '@coji/durably-react'
import type { durably } from './durably.server'

export const durably = createDurably<typeof durably>({
  api: '/api/durably',
})

// 2. Use in components
function ImportButton() {
  const { trigger, progress, isLeased, isCompleted, output } =
    durably.importCsv.useJob()

  return (
    <div>
      <button
        onClick={() => trigger({ filename: 'data.csv' })}
        disabled={isLeased}
      >
        Import
      </button>
      {progress && (
        <p>
          {progress.current}/{progress.total}
        </p>
      )}
      {isCompleted && <p>Done: {output?.count} rows</p>}
    </div>
  )
}

SPA Mode

Jobs run entirely in the browser with OPFS persistence.

tsx
import { DurablyProvider, useJob } from '@coji/durably-react/spa'
import { durably } from './lib/durably'
import { importCsvJob } from './jobs/import-csv'

function App() {
  return (
    <DurablyProvider durably={durably} fallback={<p>Loading...</p>}>
      <ImportButton />
    </DurablyProvider>
  )
}

function ImportButton() {
  const { trigger, progress, isLeased, isCompleted, output } =
    useJob(importCsvJob)

  return (
    <div>
      <button
        onClick={() => trigger({ filename: 'data.csv' })}
        disabled={isLeased}
      >
        Import
      </button>
      {progress && (
        <p>
          {progress.current}/{progress.total}
        </p>
      )}
      {isCompleted && <p>Done: {output?.count} rows</p>}
    </div>
  )
}

Available Hooks

Both Modes

HookDescription
useJobTrigger and monitor a job
useJobRunSubscribe to an existing run by ID
useJobLogsSubscribe to logs from a run
useRunsList runs with filtering and pagination

SPA Mode Only

HookDescription
useDurablyAccess the Durably instance directly

Fullstack Mode Only

HookDescription
useRunActionsRun actions (retrigger, cancel, deleteRun, getRun, getSteps) — stateless; own pending/errors at the call site

Common Patterns

Show Progress Bar

tsx
function ProgressBar({ runId }: { runId: string }) {
  const { progress, isLeased } = useJobRun({ runId })

  if (!isLeased || !progress) return null

  const percent = Math.round((progress.current / progress.total) * 100)

  return (
    <div>
      <progress value={progress.current} max={progress.total} />
      <span>
        {percent}% - {progress.message}
      </span>
    </div>
  )
}

Handle Errors

tsx
function JobRunner() {
  const { trigger, isFailed, error, reset } = useJob(myJob)

  if (isFailed) {
    return (
      <div>
        <p>Error: {error}</p>
        <button onClick={reset}>Try Again</button>
      </div>
    )
  }

  return <button onClick={() => trigger({ ... })}>Run</button>
}

Run Dashboard (Fullstack Mode)

tsx
function Dashboard() {
  const { runs, page, hasMore, nextPage, prevPage } = durably.useRuns({
    pageSize: 10,
  })
  const { retrigger, cancel, deleteRun } = durably.useRunActions()

  return (
    <table>
      <thead>
        <tr>
          <th>Job</th>
          <th>Status</th>
          <th>Actions</th>
        </tr>
      </thead>
      <tbody>
        {runs.map((run) => (
          <tr key={run.id}>
            <td>{run.jobName}</td>
            <td>{run.status}</td>
            <td>
              {run.status === 'failed' && (
                <button onClick={() => retrigger(run.id)}>Retrigger</button>
              )}
              {run.status === 'leased' && (
                <button onClick={() => cancel(run.id)}>Cancel</button>
              )}
              <button onClick={() => deleteRun(run.id)}>Delete</button>
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  )
}

Type Definitions

See Type Definitions for all exported types.

Released under the MIT License.