-
Notifications
You must be signed in to change notification settings - Fork 50
refactor: replace useActionState with useTransition for team role actions #2024
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…ions Convert form-based role change actions to imperative handlers using useTransition, providing better control over the action workflow and removing the need for form wrappers. The changes show: - Removed useActionState hook and form-based pattern - Added useTransition for managing pending states - Changed buttons from type="submit" to type="button" with explicit onClick handlers - Updated renderButton prop to pass handleClick callback instead of relying on form submission
|
|
Finished running flow.
|
PR Compliance Guide 🔍Below is a summary of compliance checks for this PR:
Compliance status legend🟢 - Fully Compliant🟡 - Partial Compliant 🔴 - Not Compliant ⚪ - Requires Further Human Verification 🏷️ - Compliance label |
||||||||||||||||||
WalkthroughThis change refactors a React component's state management approach, replacing Changes
Sequence DiagramsequenceDiagram
participant User
participant Component
participant Action
Note over Component: Before: useActionState
User->>Component: Click button (form submit)
Component->>Action: Submit action via form
Action-->>Component: Returns result
Component->>Component: Auto-updates pending state
Note over Component: After: useTransition
User->>Component: Click button (onClick handler)
Component->>Component: startTransition begins
Component->>Action: Call action async
Component->>Component: isPending = true
Action-->>Component: Returns result
Component->>Component: isPending = false
Estimated Code Review Effort🎯 2 (Simple) | ⏱️ ~8 minutes Single-file refactoring following a consistent, homogeneous pattern across all three call sites. The change involves a straightforward hook substitution with predictable API updates. Suggested Labels
Suggested Reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR refactors team role change actions from a form-based pattern using useActionState to an imperative pattern using useTransition, providing more direct control over action execution and removing unnecessary form wrappers.
Key Changes
- Replaced
useActionStatehook withuseTransitionfor managing action pending states - Converted submit buttons to regular buttons with explicit
onClickhandlers - Updated the
renderButtonprop signature to pass bothisPendingstate andhandleClickcallback
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| startTransition(async () => { | ||
| await action(); |
Copilot
AI
Oct 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The action dependency in the useCallback may cause unnecessary recreations of handleClick if the action reference changes. Additionally, startTransition should not be used with async callbacks. Consider restructuring to call the async action without awaiting inside startTransition, or add proper error handling if the action can fail.
| startTransition(async () => { | |
| await action(); | |
| action().catch((error) => { | |
| // Optionally, handle error here or propagate | |
| console.error("Error in action:", error); |
| startTransition(async () => { | ||
| await action(); |
Copilot
AI
Oct 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using async/await inside startTransition can lead to timing issues where the pending state is cleared before the async operation completes. The transition will end as soon as the async function returns its promise, not when the promise resolves. Consider wrapping the action call directly: startTransition(() => { action(); }) or handle the promise resolution explicitly if you need to track completion.
| startTransition(async () => { | |
| await action(); | |
| startTransition(() => { | |
| action(); |
PR Code Suggestions ✨Explore these optional code suggestions:
|
|||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Codex Review
Here are some automated review suggestions for this pull request.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting
| role: string; | ||
| renderButton: (isPending: boolean) => React.ReactNode; | ||
| renderButton: (options: { | ||
| isPending: boolean; | ||
| handleClick: () => void; | ||
| }) => React.ReactNode; | ||
| action: () => Promise<void>; | ||
| } | ||
| function ChangeTeamAndAction({ | ||
| renderButton, | ||
| action, | ||
| }: ChangeTeamAndActionProps) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refresh team list after imperative server action
Switching ChangeTeamAndAction to useTransition removed the <form> submission that previously triggered Next.js’s automatic refresh after a server action. The new implementation just calls action() inside startTransition and never refreshes or updates local state. For the Leave team flow this means leaveTeam runs on the server (and even calls revalidatePath("/settings/account")), but the client list never re-renders and still shows the old team until the page is manually reloaded. Consider invoking router.refresh() (or otherwise mutating local state) after the promise resolves so the UI reflects the updated membership immediately.
Useful? React with 👍 / 👎.
|
## 🔍 QA Testing Assistant by Giselle ### 📋 Manual QA Checklist Based on the changes in this PR, here are the key areas to test manually:
### ✨ Prompt for AI Agents Use the following prompts with Cursor or Claude Code to automate E2E testing: 📝 E2E Test Generation Prompt--- |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/studio.giselles.ai/app/(main)/settings/account/user-teams.tsx (1)
228-241: Remove unused props fromChangeTeamAndAction.The
teamId,userId, androleprops are declared in the interface but never used in the component implementation. They're likely remnants from the previoususeActionStatepattern.Apply this diff to clean up the interface:
interface ChangeTeamAndActionProps { - teamId: string; - userId: string; - role: string; renderButton: (options: { isPending: boolean; handleClick: () => void; }) => React.ReactNode; action: () => Promise<void>; } function ChangeTeamAndAction({ renderButton, action, }: ChangeTeamAndActionProps) {And update the three call sites to remove these props (lines 168-183, 186-201, 205-220).
🧹 Nitpick comments (1)
apps/studio.giselles.ai/app/(main)/settings/account/user-teams.tsx (1)
244-248: Consider removinguseCallbackfor minimal benefit.Since the
actionprop is an inline arrow function at each call site (e.g.,() => navigateWithChangeTeam(...)), it's recreated on every render. TheuseCallbackhere won't preventhandleClickfrom being recreated, making the optimization ineffective.You could either:
- Remove
useCallbackfor simplicity (recommended if performance is not critical)- Memoize the
actionfunctions at call sites usinguseCallbackinUserTeamsItemFor option 1:
- const handleClick = useCallback(() => { + const handleClick = () => { startTransition(() => { void action(); }); - }, [action]); + };
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/studio.giselles.ai/app/(main)/settings/account/user-teams.tsx(5 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/development-guide.mdc)
**/*.{ts,tsx}: Use Biome for formatting with tab indentation and double quotes
Follow organized imports pattern (enabled in biome.json)
Use TypeScript for type safety; avoidanytypes
Use Next.js patterns for web applications
Use async/await for asynchronous code rather than promises
Error handling: use try/catch blocks and propagate errors appropriately
Use kebab-case for all filenames (e.g.,user-profile.ts)
Use camelCase for variables, functions, and methods (e.g.,userEmail)
Use prefixes likeis,has,can,shouldfor boolean variables and functions for clarity
Use verbs or verb phrases that clearly indicate purpose for function naming (e.g.,calculateTotalPrice(), notprocess())If breaking changes are introduced in new AI SDK versions, update code to accommodate those changes
**/*.{ts,tsx}: Avoid using theanytype in TypeScript
Use async/await for asynchronous code and include proper error handling
Variables and functions should be camelCase
Boolean variables and functions should use is/has/can/should prefixes where appropriate
Function names should clearly indicate their purpose
Files:
apps/studio.giselles.ai/app/(main)/settings/account/user-teams.tsx
**/*.tsx
📄 CodeRabbit inference engine (.cursor/rules/development-guide.mdc)
**/*.tsx: Use functional components with React hooks
Use PascalCase for React components and classes (e.g.,UserProfile)
**/*.tsx: React components should use React hooks and Next.js patterns
Component identifiers (names) should be PascalCase
Files:
apps/studio.giselles.ai/app/(main)/settings/account/user-teams.tsx
**/*
📄 CodeRabbit inference engine (.cursor/rules/naming-guide.mdc)
All filenames should use kebab-case (lowercase with hyphens)
Files:
apps/studio.giselles.ai/app/(main)/settings/account/user-teams.tsx
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/naming-guide.mdc)
**/*.{js,jsx,ts,tsx}: React components and classes should use PascalCase
Variables, functions, and methods should use camelCase
Use verbs or verb phrases for function names; names should clearly indicate what the function does; avoid ambiguous names that could lead to misuse
Use nouns or noun phrases for variable names; names should describe what the variable represents; avoid single-letter variables except in very short scopes
Use prefixes like 'is', 'has', 'can', 'should' for both variables and functions returning boolean values; make the true/false meaning clear
Files:
apps/studio.giselles.ai/app/(main)/settings/account/user-teams.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: check
🔇 Additional comments (4)
apps/studio.giselles.ai/app/(main)/settings/account/user-teams.tsx (4)
4-4: LGTM!Import change correctly adds
useTransitionfor the refactored pattern.
232-236: LGTM!The updated
renderButtonsignature correctly provides bothisPendingstate andhandleClickcallback for the imperative pattern.
167-184: LGTM!All three call sites correctly updated to use the new
renderButtonAPI with object destructuring and explicitonClickhandlers. The pattern is consistent across all usages.Also applies to: 185-202, 204-221
244-248: Remove this review comment—error handling is already implemented.The concern is based on incomplete analysis. Error handling for
leaveTeamis already in place via thehandleLeaveTeamwrapper (lines 120–129), which checksresult.successand shows an error toast on failure. The other actions (navigateWithChangeTeam) throw errors thatstartTransitionhandles automatically. The suggested.catch()approach doesn't apply here because:
leaveTeamreturns a result object, not a rejected promise—it's checked at line 122navigateWithChangeTeamthrows, butstartTransitioncatches thrown errors internally without re-throwing- Wrapping with
.catch()would be redundant forleaveTeamand ineffective fornavigateWithChangeTeamLikely an incorrect or invalid review comment.
| const handleClick = useCallback(() => { | ||
| startTransition(async () => { | ||
| await action(); | ||
| }); | ||
| }, [action]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical: startTransition cannot properly handle async callbacks.
React's startTransition expects a synchronous callback. When you pass an async function, startTransition returns immediately without waiting for the promise, causing isPending to become false before the action completes. This defeats the purpose of tracking the pending state—buttons will not remain disabled during the async operation.
Wrap the async work synchronously:
const handleClick = useCallback(() => {
- startTransition(async () => {
- await action();
- });
+ startTransition(() => {
+ void action();
+ });
}, [action]);Note: void suppresses the floating promise warning. If you need to handle errors, add a try-catch inside the action or chain .catch().
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const handleClick = useCallback(() => { | |
| startTransition(async () => { | |
| await action(); | |
| }); | |
| }, [action]); | |
| const handleClick = useCallback(() => { | |
| startTransition(() => { | |
| void action(); | |
| }); | |
| }, [action]); |
🤖 Prompt for AI Agents
In apps/studio.giselles.ai/app/(main)/settings/account/user-teams.tsx around
lines 244 to 248, the current use of startTransition passes an async callback
which returns a promise immediately and causes isPending to flip false before
the async work completes; change it to pass a synchronous callback that calls
the async action without awaiting (e.g., call void action() inside the
startTransition callback) so startTransition properly tracks the transition; if
you need error handling, handle it inside the action or attach a
.catch()/try-catch within the async function rather than awaiting inside
startTransition.
User description
Summary
Convert form-based role change actions to imperative handlers using useTransition, providing better control over the action workflow and removing the need for form wrappers.
Changes
Testing
Other Information
PR Type
Enhancement
Description
Replace
useActionStatewithuseTransitionfor imperative action handlingConvert form-based pattern to explicit button click handlers
Update
renderButtonprop to passhandleClickcallback andisPendingstateRemove form wrapper element from component structure
Diagram Walkthrough
File Walkthrough
user-teams.tsx
Convert form actions to imperative useTransition patternapps/studio.giselles.ai/app/(main)/settings/account/user-teams.tsx
useActionStateimport withuseTransitionChangeTeamAndActionPropsinterface to pass object withisPendingandhandleClicktorenderButtonChangeTeamAndActioncomponent from form-based to imperativepattern using
useTransitionanduseCallbacktype="submit"totype="button"withonClick={handleClick}handlersSummary by CodeRabbit