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:

QuestionBrowser HooksServer 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 Browser 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'

Browser Hooks Reference →

Choose Server 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 { createDurablyClient } from '@coji/durably-react/client'

Server Hooks Reference →

Installation

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

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

Quick Examples

Browser Mode

Jobs run entirely in the browser with OPFS persistence.

tsx
import { DurablyProvider, useJob } from '@coji/durably-react'
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, isRunning, isCompleted, output } = useJob(importCsvJob)

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

Server Mode

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

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

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

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

  return (
    <div>
      <button onClick={() => trigger({ filename: 'data.csv' })} disabled={isRunning}>
        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

Browser Mode Only

HookDescription
useDurablyAccess the Durably instance directly

Server Mode Only

HookDescription
useRunActionsRetry, cancel, delete runs

Common Patterns

Show Progress Bar

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

  if (!isRunning || !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 (Server Mode)

tsx
function Dashboard() {
  const { runs, page, hasMore, nextPage, prevPage } = useRuns({
    api: '/api/durably',
    pageSize: 10,
  })
  const { retry, cancel, deleteRun } = useRunActions({ api: '/api/durably' })

  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={() => retry(run.id)}>Retry</button>}
              {run.status === 'running' && <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.