Skip to content

feat: redesign durably-react hooks for React 19#179

Merged
coji merged 7 commits into
mainfrom
feat/react19-hooks-redesign
Mar 29, 2026
Merged

feat: redesign durably-react hooks for React 19#179
coji merged 7 commits into
mainfrom
feat/react19-hooks-redesign

Conversation

@coji
Copy link
Copy Markdown
Owner

@coji coji commented Mar 29, 2026

Summary

Closes #160

  • Remove useRunActions state: isLoading and error removed from return type — callers use React 19 useTransition for per-button pending UI and try/catch for errors
  • Transparent wrapper in createJobHooks: options forwarded via Omit<Options, 'api' | ...> so new options auto-propagate
  • isTerminal / isActive on hooks and ClientRun: derived status booleans replace manual status enumeration

Breaking Changes

Before After
useRunActions() returns isLoading, error Returns only action methods; use useTransition + local state
run.status === 'completed' || ... run.isTerminal
createJobHooks().useRun(id, { onComplete }) only Forwards all UseJobRunClientOptions except api/runId

Test plan

  • pnpm validate passes (format, lint, typecheck, tests — 28/28)
  • Type tests cover new API surface (UseRunActionsClientResult keys, isTerminal/isActive on hooks)
  • Example dashboards use useTransition + Set<string> busy tracking
  • find-stale.sh doc check clean
  • website/public/llms.txt regenerated

🤖 Generated with Claude Code

Remove loading/error state from useRunActions (callers use useTransition),
add isTerminal/isActive to hooks and ClientRun, make createJobHooks
forward options transparently. Bump to 0.15.0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
durably-demo Ready Ready Preview Mar 29, 2026 8:20am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
durably-demo-vercel-turso Ignored Ignored Preview Mar 29, 2026 8:20am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 29, 2026

📝 Walkthrough

Walkthrough

durably-react ライブラリを React 19 向けに再設計し、ホック群に isTerminal / isActive 派生プロパティを追加、useRunActions からホック級のローディング状態を削除、createJobHooks のオプション転送パターンを改善、ダッシュボード例を useTransition ベースの状態管理に更新。

Changes

コホート / ファイル 概要
コア型・インターフェース更新
packages/durably-react/src/client/use-job-run.ts, packages/durably-react/src/client/use-job.ts, packages/durably-react/src/hooks/use-job-run.ts, packages/durably-react/src/hooks/use-job.ts, packages/durably/src/storage.ts
UseJobRunResultUseJobResult インターフェースに isTerminal および isActive ブール値を追加。ClientRun 型も同様に拡張。これらは status から派生(terminal: completed/failed/cancelled、active: pending/leased)。
useRunActions ホック再設計
packages/durably-react/src/client/use-run-actions.ts
ホック級のローディング状態と共有エラーを削除。戻り値を retrigger, cancel, deleteRun, getRun, getSteps メソッドのみに限定。エラーハンドリングを呼び出し側に委譲(useTransition + try/catch)。
createJobHooks 透過型ラッパーパターン
packages/durably-react/src/client/create-job-hooks.ts
useJob, useRun, useLogs 署名を更新。api / jobName / runId を除外した Omit オプション型を受け入れ、自動転送。
型・型定義の再構成
packages/durably-react/src/types.ts, website/api/durably-react/types.md, website/api/durably/types.md
ローカル RunStatus 宣言を削除、@coji/durably からの再エクスポートに変更。ClientRun ドキュメントを更新し、新しい派生フィールドを記載。
ダッシュボード例の更新
examples/fullstack-react-router/app/routes/_index/dashboard.tsx, examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx
useRunActions の廃止された isLoading を削除。useTransitionisPending を管理、busyKeys: Set<string> で個別アクション状態を追跡。runAction ヘルパーで async トランジション・エラーハンドリングをカプセル化。isActive/isTerminal で状態判定条件を置き換え。
テスト追加・更新
packages/durably-react/tests/browser/*, packages/durably-react/tests/client/*
isTerminal, isActive の新しいアサーション追加。useRunActions テストを再構成し、ホック級ローディング状態の検証を削除、アクション拒否ハンドリングのテストを強化。
ドキュメント・ガイド更新
packages/durably-react/docs/llms.md, website/api/durably-react/fullstack.md, website/api/durably-react/spa.md, website/guide/error-handling.md, website/guide/fullstack-mode.md, website/public/llms.txt
useRunActions 使用例を useTransition + 手動エラー捕捉パターンに書き換え。isTerminal/isActive の新プロパティを記載。createJobHooks オプション転送パターンを説明。
パッケージ版バージョン更新
packages/durably-react/package.json, packages/durably/package.json
@coji/durably-react@coji/durably をバージョン 0.14.0 から 0.15.0 に更新。

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PR #105: fullstack-vercel-turso ダッシュボード実装の直接修正。useTransition ベース状態管理、per-action busy tracking、isActive/isTerminal 活用が追加。
  • PR #82: useRunActions API 再設計と同一ファイル内の多数の修正。ホック級ローディング削除、呼び出し側での useTransition 管理移行をカバー。
  • PR #155: createJobHooks オプション転送パターンの直接関連。useRun コールバック・オプション転送ロジックを改善する同じコードパス。

Poem

🐰 ホックの衣を脱ぎ去りて、
状態の荷は呼び手へ託す。
ターミナル、アクティブ、真理の声、
React 19 の光に照らされて、
設計は透き通り、清く。 ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat: redesign durably-react hooks for React 19' accurately captures the main objective of this comprehensive redesign, clearly communicating the primary change at a high level.
Linked Issues check ✅ Passed The PR successfully addresses all major coding requirements from issue #160: removes loading/error state from useRunActions, implements transparent wrapper pattern for createJobHooks with Omit options forwarding, adds isTerminal/isActive derived properties to ClientRun and hook results, and updates all examples and documentation accordingly.
Out of Scope Changes check ✅ Passed All code changes are directly scoped to issue #160's objectives: hooks redesign, derived status properties, createJobHooks transparency, documentation updates, and example modernization; no unrelated changes detected.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/react19-hooks-redesign

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

ClientRun now includes isTerminal/isActive derived fields.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
coji and others added 2 commits March 29, 2026 16:45
- Fix delete → deleteRun naming in index.md hook table
- Handle getRun returning null in showDetails (both dashboards)
- Show detailError outside modal when selectedRun is null

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Version bumps will be done in a separate release PR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (4)
packages/durably-react/tests/client/create-job-hooks.test.tsx (1)

83-100: このテストは「オプション転送」を実証できていません。

Line 83-100 は trigger の存在確認のみで、followLatest / autoResume が実際に下位 useJob に渡ったかは検証できません。転送バグがあっても通るため、意図に対して弱いです。

✅ 期待挙動を直接検証する差分例
 it('forwards optional useJob options to the underlying hook', async () => {
   const fetchMock = vi.fn().mockResolvedValue({
     ok: true,
     json: () => Promise.resolve({ runId: 'csv-run-id' }),
   })
   globalThis.fetch = fetchMock

   const hooks = createJobHooks<typeof importCsvJob>({
     api: '/api/durably',
     jobName: 'import-csv',
   })

   const { result } = renderHook(() =>
     hooks.useJob({ followLatest: false, autoResume: false }),
   )

-  expect(result.current.trigger).toBeTypeOf('function')
+  // autoResume が無効なら mount 時に runs 取得しない
+  expect(fetchMock).not.toHaveBeenCalled()
+
+  await result.current.trigger({ filename: 'data.csv' })
+  expect(fetchMock).toHaveBeenCalledWith(
+    '/api/durably/trigger',
+    expect.objectContaining({ method: 'POST' }),
+  )
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/durably-react/tests/client/create-job-hooks.test.tsx` around lines
83 - 100, The test only checks trigger exists but doesn't assert the optional
flags are forwarded; update the test that uses createJobHooks and hooks.useJob
to actually call result.current.trigger(...) (or otherwise execute the
underlying request) and then assert the mocked fetch (fetchMock) received a
request whose payload or query includes the followLatest and autoResume values
you passed to hooks.useJob({ followLatest: false, autoResume: false });
specifically, after renderHook(() => hooks.useJob(...)) invoke the hook action
(trigger) and inspect fetchMock.mock.calls to verify the request body or URL
contains followLatest and autoResume, using the symbols createJobHooks,
hooks.useJob, trigger, followLatest, and autoResume to locate the relevant code.
packages/durably-react/src/client/use-run-actions.ts (1)

68-100: ドキュメント内の使用例が新しいパターンを適切に示しています。

useTransition を使用したペンディング状態の管理と .catch() によるエラーハンドリングが示されており、React 19 のベストプラクティスに沿っています。

ただし、例の中で .catch(() => {}) は空のハンドラーになっています。本番コードでは適切なエラーハンドリング(ユーザーへの通知やログ記録)が必要です。ドキュメント例としては簡潔さのために許容されますが、コメントで本番では適切なエラー処理が必要であることを示唆することを検討してください。

💡 より明確な例への修正案
 *         <button
 *           onClick={() =>
-*             startTransition(() => retrigger(runId).catch(() => {}))
+*             startTransition(() => retrigger(runId).catch(console.error))
 *           }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/durably-react/src/client/use-run-actions.ts` around lines 68 - 100,
Update the example in the RunActions component that uses useRunActions,
retrigger, cancel and useTransition to note that the current .catch(() => {}) is
only for brevity; modify the JSX example near RunActions to replace the empty
catch with a brief inline comment (or a short example call) indicating that
production code should perform proper error handling (e.g., logging or user
notification) when calling retrigger(runId) and cancel(runId), and ensure
references to isPending and startTransition remain unchanged so the example
still demonstrates transition handling correctly.
website/guide/error-handling.md (1)

64-72: エラーハンドリングパターンの一貫性について検討してください。

Retriggerボタン(Lines 49-54)では try/catch を使用していますが、Cancelボタンでは void ... .catch() パターンを使用しています。両方とも有効なアプローチですが、同じドキュメント内で異なるパターンを使用すると読者が混乱する可能性があります。

教育目的で両方のパターンを意図的に示している場合は、そのことを明示的にコメントで説明するか、どちらか一方に統一することを検討してください。

💡 一貫性のある例への修正案
   if (status === 'leased') {
     return (
       <button
-        onClick={() => {
-          void cancel(runId).catch((e) => console.error('Cancel failed:', e))
-        }}
+        onClick={async () => {
+          try {
+            await cancel(runId)
+          } catch (e) {
+            console.error('Cancel failed:', e)
+          }
+        }}
       >
         Cancel
       </button>
     )
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/guide/error-handling.md` around lines 64 - 72,
ドキュメント内のエラーハンドリングが一貫しておらず読者が混乱するので、Cancelボタンの onClick(現在の void
cancel(runId).catch(...))を Retrigger ボタンで使っている try/catch
パターンに統一するか、両方を並べて示す意図があることを明示するコメントを追加してください;具体的には cancel(runId) 呼び出しを async
ハンドラ内で try { await cancel(runId) } catch (e) { console.error('Cancel failed:',
e) } の形式に変更するか、両方を並列で示すセクションに「意図的に異なるパターンを比較している」旨の短い説明を加えてください。
packages/durably-react/tests/types.test.ts (1)

125-153: createJobHooks.useLogs の型もここで固定しておきたいです。

create-job-hooks.ts では useLogsOmit<UseJobLogsClientOptions, 'api' | 'runId'> に変わっているので、このスイートが useJob / useRun だけを固定していると残り 1 本の透過ラッパーだけ回帰を見逃します。Hooks['useLogs'] の引数型も同じ形で追加しておくと安心です。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/durably-react/tests/types.test.ts` around lines 125 - 153, The test
suite pins types for useJob and useRun but misses useLogs; update the tests to
also assert Hooks['useLogs'] parameter type matches
Omit<UseJobLogsClientOptions, 'api' | 'runId'> | undefined so changes in
createJobHooks propagate to tests. Locate the test block for createJobHooks
(where JobHooks is aliased to Hooks) and add an
expectTypeOf<Hooks['useLogs']>().parameter(0).toEqualTypeOf< Omit<
import('../src/client/use-job-logs').UseJobLogsClientOptions, 'api' | 'runId' >
| undefined >() referencing the Hooks type and UseJobLogsClientOptions to lock
the wrapper’s argument shape.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/fullstack-react-router/app/routes/_index/dashboard.tsx`:
- Around line 96-110: Clear previous detail state when starting and on failure,
and ensure the error is surfaced independently of the selectedRun modal. In
showDetails(), before calling getRun/getSteps call setDetailError(null),
setDetailLoading(true), setSteps([]) and setSelectedRun(null) so stale
steps/selectedRun aren’t shown if fetch fails; in the catch block keep calling
setDetailError(e instanceof Error ? e.message : String(e)) and also setSteps([])
and setSelectedRun(null) to fully reset detail state on error. Finally, update
UI usage so it reads the standalone detailError state (not a field under
selectedRun) so errors are visible even when the modal/selectedRun is not set;
apply the same fixes to the other occurrence referenced at lines ~317-319 (same
showDetails usage).

In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx`:
- Around line 109-123: The showDetails error path can leave stale detail state
(previous steps/selectedRun) visible if getRun/getSteps fails; fix by resetting
detail state at the start of the load: call setSteps([]), setSelectedRun(null)
and setDetailError(null) before setDetailLoading(true) (so the UI clears
previous data and will render errors even when selectedRun is null), then
perform getRun/getSteps in the try and on catch setDetailError(e instanceof
Error ? e.message : String(e)) and ensure setSteps([]) / setSelectedRun(null)
remain if the fetch fails.

In `@packages/durably-react/src/client/create-job-hooks.ts`:
- Around line 95-110: The factory currently spreads fixed values (api, jobName,
runId) before caller options which lets caller options accidentally overwrite
those fixed values; update the three wrappers so caller-provided options are
spread first and the fixed values are spread last. Specifically, in the useJob
wrapper (useJob: (jobOptions) => ...), spread jobOptions before adding api and
jobName; in useRun (useRun: (runId, runOptions) => ...) spread runOptions before
adding api and runId; and in useLogs (useLogs: (runId, logsOptions) => ...)
spread logsOptions before adding api and runId so the wrapper’s
api/jobName/runId cannot be overridden by caller options.

In `@packages/durably-react/tests/client/use-run-actions.test.tsx`:
- Around line 244-273: The test's unhandledrejection listener can miss
rejections because retrigger('run-123') involves multiple awaits; capture the
Promise returned by retrigger (from the onClick call in Harness) outside the act
block, await it (with a try/catch if needed) before removing the listener, and
move globalThis.removeEventListener('unhandledrejection', onUnhandled) into a
finally block so the listener is only removed after the retrigger promise has
fully settled; refer to the retrigger call in Harness, the onUnhandled listener
and the unhandled array when making this change.

In `@website/api/http-handler.md`:
- Line 100: The documentation's ClientRun field exclusion list omits leaseOwner,
which can mislead readers about what /runs and /run return; update the
descriptive list (the sentence that lists internal fields for ClientRun) to
include leaseOwner alongside leaseExpiresAt, idempotencyKey, concurrencyKey,
leaseGeneration, updatedAt, etc., and ensure the note about using toClientRun()
mentions that leaseOwner is also stripped by that projection; apply the same
amendment to the second occurrence referenced (the other sentence near line
106).

---

Nitpick comments:
In `@packages/durably-react/src/client/use-run-actions.ts`:
- Around line 68-100: Update the example in the RunActions component that uses
useRunActions, retrigger, cancel and useTransition to note that the current
.catch(() => {}) is only for brevity; modify the JSX example near RunActions to
replace the empty catch with a brief inline comment (or a short example call)
indicating that production code should perform proper error handling (e.g.,
logging or user notification) when calling retrigger(runId) and cancel(runId),
and ensure references to isPending and startTransition remain unchanged so the
example still demonstrates transition handling correctly.

In `@packages/durably-react/tests/client/create-job-hooks.test.tsx`:
- Around line 83-100: The test only checks trigger exists but doesn't assert the
optional flags are forwarded; update the test that uses createJobHooks and
hooks.useJob to actually call result.current.trigger(...) (or otherwise execute
the underlying request) and then assert the mocked fetch (fetchMock) received a
request whose payload or query includes the followLatest and autoResume values
you passed to hooks.useJob({ followLatest: false, autoResume: false });
specifically, after renderHook(() => hooks.useJob(...)) invoke the hook action
(trigger) and inspect fetchMock.mock.calls to verify the request body or URL
contains followLatest and autoResume, using the symbols createJobHooks,
hooks.useJob, trigger, followLatest, and autoResume to locate the relevant code.

In `@packages/durably-react/tests/types.test.ts`:
- Around line 125-153: The test suite pins types for useJob and useRun but
misses useLogs; update the tests to also assert Hooks['useLogs'] parameter type
matches Omit<UseJobLogsClientOptions, 'api' | 'runId'> | undefined so changes in
createJobHooks propagate to tests. Locate the test block for createJobHooks
(where JobHooks is aliased to Hooks) and add an
expectTypeOf<Hooks['useLogs']>().parameter(0).toEqualTypeOf< Omit<
import('../src/client/use-job-logs').UseJobLogsClientOptions, 'api' | 'runId' >
| undefined >() referencing the Hooks type and UseJobLogsClientOptions to lock
the wrapper’s argument shape.

In `@website/guide/error-handling.md`:
- Around line 64-72: ドキュメント内のエラーハンドリングが一貫しておらず読者が混乱するので、Cancelボタンの onClick(現在の
void cancel(runId).catch(...))を Retrigger ボタンで使っている try/catch
パターンに統一するか、両方を並べて示す意図があることを明示するコメントを追加してください;具体的には cancel(runId) 呼び出しを async
ハンドラ内で try { await cancel(runId) } catch (e) { console.error('Cancel failed:',
e) } の形式に変更するか、両方を並列で示すセクションに「意図的に異なるパターンを比較している」旨の短い説明を加えてください。
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 63b1f8fa-93cf-44b7-801c-0bebd9b01b61

📥 Commits

Reviewing files that changed from the base of the PR and between fcd6aa5 and 4fd86f7.

📒 Files selected for processing (32)
  • examples/fullstack-react-router/app/routes/_index/dashboard.tsx
  • examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx
  • packages/durably-react/docs/llms.md
  • packages/durably-react/package.json
  • packages/durably-react/src/client/create-job-hooks.ts
  • packages/durably-react/src/client/use-job-run.ts
  • packages/durably-react/src/client/use-job.ts
  • packages/durably-react/src/client/use-run-actions.ts
  • packages/durably-react/src/hooks/use-job-run.ts
  • packages/durably-react/src/hooks/use-job.ts
  • packages/durably-react/src/types.ts
  • packages/durably-react/tests/browser/use-job-run.test.tsx
  • packages/durably-react/tests/browser/use-job.test.tsx
  • packages/durably-react/tests/client/create-job-hooks.test.tsx
  • packages/durably-react/tests/client/use-job-run.test.tsx
  • packages/durably-react/tests/client/use-job.test.tsx
  • packages/durably-react/tests/client/use-run-actions.test.tsx
  • packages/durably-react/tests/client/use-runs.test.tsx
  • packages/durably-react/tests/types.test.ts
  • packages/durably/docs/llms.md
  • packages/durably/package.json
  • packages/durably/src/storage.ts
  • packages/durably/tests/shared/server.shared.ts
  • website/api/durably-react/fullstack.md
  • website/api/durably-react/index.md
  • website/api/durably-react/spa.md
  • website/api/durably-react/types.md
  • website/api/http-handler.md
  • website/api/index.md
  • website/guide/error-handling.md
  • website/guide/fullstack-mode.md
  • website/public/llms.txt

Comment thread examples/fullstack-react-router/app/routes/_index/dashboard.tsx
Comment thread examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx
Comment thread packages/durably-react/src/client/create-job-hooks.ts
Comment thread packages/durably-react/tests/client/use-run-actions.test.tsx
Comment thread website/api/http-handler.md Outdated
…ntRun exclusion list

- Clear selectedRun and steps at start of showDetails to prevent stale data on error
- Add leaseOwner to ClientRun excluded fields list in http-handler.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coji coji force-pushed the feat/react19-hooks-redesign branch from cd1800b to d6ae06d Compare March 29, 2026 08:15
- JSDoc example: replace empty .catch(() => {}) with .catch(console.error)
- createJobHooks test: assert autoResume:false prevents fetch on mount
- Type test: add useLogs wrapper parameter assertion
- error-handling guide: unify cancel to try/catch pattern

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coji coji force-pushed the feat/react19-hooks-redesign branch from 870a407 to abee861 Compare March 29, 2026 08:19
@coji coji merged commit 7ade0c8 into main Mar 29, 2026
5 checks passed
@coji coji deleted the feat/react19-hooks-redesign branch March 29, 2026 09:44
@coji coji mentioned this pull request Mar 29, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: redesign durably-react hooks for React 19

1 participant