Skip to content

feat(react): use useTransition within useServerAction#459

Merged
dinwwwh merged 2 commits intomainfrom
issue/451
May 2, 2025
Merged

feat(react): use useTransition within useServerAction#459
dinwwwh merged 2 commits intomainfrom
issue/451

Conversation

@dinwwwh
Copy link
Copy Markdown
Member

@dinwwwh dinwwwh commented Apr 30, 2025

Closes: https://github.com/unnoq/orpc/issues/451

Summary by CodeRabbit

  • New Features

    • Improved responsiveness of server action hooks by integrating asynchronous state management for pending states.
  • Refactor

    • Enhanced internal handling of action state updates for smoother user experience during server actions.
  • Tests

    • Added tests covering error handling, concurrent executions, and reset behavior during server actions.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 30, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
orpc ✅ Ready (Inspect) Visit Preview 💬 Add feedback Apr 30, 2025 2:04pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 30, 2025

Walkthrough

The useServerAction React hook was refactored to utilize React's useTransition API for managing asynchronous state transitions. The pending state is now handled using a constant and React's transition mechanism, rather than directly updating state. The executedAt timestamp and input are stored in refs instead of state and are reset with a reset callback. The hook's return value is memoized to reflect the current state, pending status, and the latest input and execution timestamp. The function signature for useServerAction remains unchanged.

Changes

File(s) Change Summary
packages/react/src/hooks/action-hooks.ts Refactored useServerAction to use useTransition; moved executedAt and input to refs; updated pending state handling; introduced PENDING_STATE constant; internal implementation changes only, no API signature change.
packages/react/src/hooks/action-hooks.test.tsx Added global mock reset in tests; introduced reusable mocked action handler; added tests for synchronous action errors, concurrency with multiple executes, and reset behavior during execution; improved test coverage for error handling and state management.

Sequence Diagram(s)

sequenceDiagram
    participant Component
    participant useServerAction
    participant Action
    Note over Component: User calls execute(input)
    Component->>useServerAction: execute(input)
    useServerAction->>useServerAction: startTransition(async action)
    useServerAction->>Action: Perform async action
    Action-->>useServerAction: Return result/error
    useServerAction->>useServerAction: Update refs (executedAt, input)
    useServerAction-->>Component: Resolve Promise with result
    Component->>useServerAction: reset()
    useServerAction->>useServerAction: Reset refs
Loading

Assessment against linked issues

Objective Addressed Explanation
Integrate React.useTransition() and startTransition in useServerAction() (#451)
Consideration of additional hooks like useOptimisticAction and useStateAction (#451) No new hooks like useOptimisticAction or useStateAction were added.

Poem

A hop and a skip, transitions anew,
Now actions await as React tells them to.
With refs for the time and the input you send,
Pending is constant, no state to amend.
The rabbit approves, with a twitch of its nose—
Async and smooth, that's how progress goes!
🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 30, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

📢 Thoughts on this report? Let us know!

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 30, 2025

More templates

@orpc/arktype

npm i https://pkg.pr.new/@orpc/arktype@459

@orpc/client

npm i https://pkg.pr.new/@orpc/client@459

@orpc/contract

npm i https://pkg.pr.new/@orpc/contract@459

@orpc/openapi

npm i https://pkg.pr.new/@orpc/openapi@459

@orpc/react

npm i https://pkg.pr.new/@orpc/react@459

@orpc/openapi-client

npm i https://pkg.pr.new/@orpc/openapi-client@459

@orpc/react-query

npm i https://pkg.pr.new/@orpc/react-query@459

@orpc/server

npm i https://pkg.pr.new/@orpc/server@459

@orpc/shared

npm i https://pkg.pr.new/@orpc/shared@459

@orpc/solid-query

npm i https://pkg.pr.new/@orpc/solid-query@459

@orpc/standard-server

npm i https://pkg.pr.new/@orpc/standard-server@459

@orpc/standard-server-fetch

npm i https://pkg.pr.new/@orpc/standard-server-fetch@459

@orpc/standard-server-node

npm i https://pkg.pr.new/@orpc/standard-server-node@459

@orpc/svelte-query

npm i https://pkg.pr.new/@orpc/svelte-query@459

@orpc/valibot

npm i https://pkg.pr.new/@orpc/valibot@459

@orpc/vue-colada

npm i https://pkg.pr.new/@orpc/vue-colada@459

@orpc/vue-query

npm i https://pkg.pr.new/@orpc/vue-query@459

@orpc/zod

npm i https://pkg.pr.new/@orpc/zod@459

commit: 8b6f088

Copy link
Copy Markdown

@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: 2

🧹 Nitpick comments (4)
packages/react/src/hooks/action-hooks.ts (4)

83-91: Consider freezing PENDING_STATE to avoid accidental mutation

PENDING_STATE is intended to be a constant baseline snapshot.
Because it is later spread into the returned object (...currentState), an inadvertent mutation would affect every subsequent result.
You can safeguard against this by making it immutable:

-const PENDING_STATE = {
+const PENDING_STATE = Object.freeze({
   data: undefined,
   error: null,
   isIdle: false,
   isPending: true,
   isSuccess: false,
   isError: false,
   status: 'pending',
-}
+}) as const

105-110: Typing omission hides executedAt & input from state but not from business logic

State is now declared with Omit<...,'executedAt' | 'input'>, which matches the ref-based design.
However, nothing prevents future contributors from mistakenly trying to setState({ executedAt }) and wondering why TS complains.

A small doc‐comment above INITIAL_STATE or the state declaration explaining that “executedAt and input deliberately live in refs to avoid re-renders” would save time.
Alternatively, extract the state type into a named alias (InternalActionState) to make the omission explicit.


122-153: Promise wrapper leaks unhandled rejection on interceptor error paths

Inside startTransition you wrap the async body with safe(), so most errors are captured.
However, if React itself throws (e.g., during setState) or an unexpected runtime error escapes before safe, the outer Promise will neither resolve nor reject, leaving callers hanging.

You can propagate fatal errors by surrounding the await block with try/catch and calling reject:

-return new Promise((resolve) => {
+return new Promise((resolve, reject) => {
   startTransition(async () => {
-    const result = await safe(/* … */)
-    setState({ … })
-    resolve(result)
+    try {
+      const result = await safe(/* … */)
+      setState({ … })
+      resolve(result)
+    } catch (err) {
+      reject(err)
+      throw err          // still surface to React error boundaries
+    }
   })
 })

This keeps the hook predictable for imperative consumers.


155-167: useMemo dependencies omit options.interceptors mutations

execute recompiles when the values inside options.interceptors change (spread into the memo list earlier), but result only depends on reset and execute.
If the parent component swaps interceptors, the returned execute is fresh, yet the outer result object (and referential equality) remains unchanged. Downstream useEffect hooks that rely on result.execute identity may miss the update.

Safest path: include execute alone (it already closes over updated interceptors) and document that execute identity can change when options.interceptors mutate, or explicitly add a // eslint-disable-next-line react-hooks/exhaustive-deps with rationale.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7b73a06 and 12d7c50.

📒 Files selected for processing (1)
  • packages/react/src/hooks/action-hooks.ts (3 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: publish-commit

Copy link
Copy Markdown

@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: 0

🧹 Nitpick comments (1)
packages/react/src/hooks/action-hooks.ts (1)

122-160: Potential race condition with timestamp-based execution tracking

The implementation addresses race conditions by tracking the execution timestamp, but using a timestamp as an execution ID could potentially lead to collisions if two executions happen in the exact same millisecond.

Consider using a more unique identifier like a Symbol or incrementing number instead:

+const executionIdRef = useRef(0)

const execute = useCallback(async (input: TInput, executeOptions: UseServerActionExecuteOptions<TInput, TOutput, UnactionableError<TError>> = {}) => {
+  const currentExecutionId = ++executionIdRef.current
   const executedAt = new Date()
   executedAtRef.current = executedAt
   // ...

   return new Promise((resolve) => {
     startTransition(async () => {
       // ...
-      if (executedAtRef.current === executedAt) {
+      if (executionIdRef.current === currentExecutionId) {
         setState({
           // ...
         })
       }
       // ...
     })
   })
}, [/* ... */])
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 12d7c50 and 8b6f088.

📒 Files selected for processing (2)
  • packages/react/src/hooks/action-hooks.test.tsx (3 hunks)
  • packages/react/src/hooks/action-hooks.ts (3 hunks)
🔇 Additional comments (10)
packages/react/src/hooks/action-hooks.ts (5)

6-6: Import dependency updated to include useTransition

The import now includes useTransition from React, which is used to manage asynchronous state transitions.


83-91: Well-structured PENDING_STATE constant

Good addition of a separate constant for pending state, which helps improve code readability and maintainability.


112-114: Better state management with useTransition and refs

Moving executedAt to a ref and using React's useTransition is a good improvement for managing asynchronous state updates, allowing React to better prioritize renders.


116-120: Reset function doesn't cancel in-flight transitions

The current reset implementation clears refs and state but doesn't actually cancel pending transitions. When an in-flight action completes after reset, its callback still executes (though state updates are prevented).

Consider enhancing the reset function to support true cancellation:

+const currentExecutionRef = useRef<symbol | null>(null)

const execute = useCallback(async (input: TInput, executeOptions: UseServerActionExecuteOptions<TInput, TOutput, UnactionableError<TError>> = {}) => {
+  const execId = Symbol('execution')
+  currentExecutionRef.current = execId
   const executedAt = new Date()
   executedAtRef.current = executedAt
   // ...

   return new Promise((resolve) => {
     startTransition(async () => {
       // ...
-      if (executedAtRef.current === executedAt) {
+      if (currentExecutionRef.current === execId) {
         setState({
           // ...
         })
       }
       // ...
     })
   })
}, [/* ... */])

const reset = useCallback(() => {
  executedAtRef.current = undefined
+  currentExecutionRef.current = null
  setInput(undefined)
  setState({ ...INITIAL_STATE })
}, [])

This would provide a more robust way to handle cancellation, as mentioned in previous review comments.


162-174: Good use of useMemo for result derivation

The useMemo hook efficiently combines state, refs, and functions into a consistent result object. The conditional selection between PENDING_STATE and regular state based on isPending is well implemented.

packages/react/src/hooks/action-hooks.test.tsx (5)

6-8: Good test hygiene with beforeEach

Adding a global beforeEach to clear all mocks ensures a clean slate for each test, preventing cross-test contamination.


11-13: Improved test structure with reusable mock handler

Extracting the handler into a reusable mock function makes the tests more maintainable and allows for dynamic behavior changes in individual tests.


119-168: Good coverage for synchronous action errors

This test properly verifies that synchronous errors in the action are correctly handled by the hook's state transitions.


211-269: Thorough testing of concurrent executions

This test effectively verifies that multiple parallel executions work correctly, with each promise resolving independently and only the latest execution affecting the final state.


271-312: Good coverage of reset behavior during execution

This test confirms that reset immediately clears state even during a pending transition, and that subsequent state updates from in-flight actions are properly ignored.

However, note that this test also reinforces what was mentioned earlier - reset doesn't actually cancel the in-flight operation, it just prevents state updates. The promise still resolves with the expected result (line 304).

@dinwwwh dinwwwh merged commit 95b67ec into main May 2, 2025
8 checks passed
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.

Why no React.useTransition() in useServerAction()?

1 participant