Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { MoreHorizontal, Search } from "lucide-react";
import { useActionState, useCallback, useState } from "react";
import { useCallback, useState, useTransition } from "react";
import { FreeTag } from "@/components/free-tag";
import { ProTag } from "@/components/pro-tag";
import {
Expand Down Expand Up @@ -169,9 +169,10 @@ function UserTeamsItem({
teamId={teamId}
userId={currentUserId}
role={role}
renderButton={(isPending) => (
renderButton={({ isPending, handleClick }) => (
<button
type="submit"
type="button"
onClick={handleClick}
className="flex items-center w-full px-3 py-2 text-left text-[14px] leading-[16px] hover:bg-white/5 text-white-400 rounded-md"
disabled={isPending}
>
Expand All @@ -186,9 +187,10 @@ function UserTeamsItem({
teamId={teamId}
userId={currentUserId}
role={role}
renderButton={(isPending) => (
renderButton={({ isPending, handleClick }) => (
<button
type="submit"
type="button"
onClick={handleClick}
className="flex items-center w-full px-3 py-2 text-left text-[14px] leading-[16px] hover:bg-white/5 text-white-400 rounded-md"
disabled={isPending}
>
Expand All @@ -204,9 +206,10 @@ function UserTeamsItem({
teamId={teamId}
userId={currentUserId}
role={role}
renderButton={(isPending) => (
renderButton={({ isPending, handleClick }) => (
<button
type="submit"
type="button"
onClick={handleClick}
className="flex items-center w-full px-3 py-2 font-medium text-[14px] leading-[16px] text-error-900 hover:bg-error-900/20 rounded-md"
disabled={isPending}
>
Expand All @@ -226,18 +229,23 @@ interface ChangeTeamAndActionProps {
teamId: string;
userId: string;
role: string;
renderButton: (isPending: boolean) => React.ReactNode;
renderButton: (options: {
isPending: boolean;
handleClick: () => void;
}) => React.ReactNode;
action: () => Promise<void>;
}
function ChangeTeamAndAction({
renderButton,
action,
}: ChangeTeamAndActionProps) {
Comment on lines 231 to 241

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

const [_state, formAction, isPending] = useActionState(action, null);
const [isPending, startTransition] = useTransition();

return (
<form className="w-full" action={formAction}>
{renderButton(isPending)}
</form>
);
const handleClick = useCallback(() => {
startTransition(async () => {
await action();
Comment on lines +245 to +246
Copy link

Copilot AI Oct 17, 2025

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.

Suggested change
startTransition(async () => {
await action();
action().catch((error) => {
// Optionally, handle error here or propagate
console.error("Error in action:", error);

Copilot uses AI. Check for mistakes.
Comment on lines +245 to +246
Copy link

Copilot AI Oct 17, 2025

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.

Suggested change
startTransition(async () => {
await action();
startTransition(() => {
action();

Copilot uses AI. Check for mistakes.
});
}, [action]);
Comment on lines +244 to +248
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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.

Suggested change
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.


return renderButton({ isPending, handleClick });
}