Issue with Server Actions + useTransition hanging indefinitely #88767
SummaryI’m running into a specific issue where my UI gets stuck in a pending state after a Server Action completes. The backend updates successfully, but the UI does not update until I manually reload the page. This issue is intermittent. It works correctly about 7 out of 10 times, but the other 30% of the time the useTransition hook never resolves, leaving the spinner spinning indefinitely. Context: cart-quantity.tsx
"use client";
import { updateCartItem } from "@/actions/cart";
import { useTransition } from "react";
// ... imports
export const CartQuantity = ({ item }: { item: CartItemWithProduct }) => {
const [pending, startTransition] = useTransition();
const handleQuantityChange = (quantity: number) => {
if (pending) return;
startTransition(async () => {
// The await completes, but the transition doesn't seem to "finish"
await updateCartItem(item.productId, item.quantity + quantity);
});
};
return (
<div className="flex items-center space-x-2">
{/* ... buttons ... */}
<span className="w-12 text-center font-bold">
{pending ? <Loader2 className="animate-spin" /> : item.quantity}
</span>
{/* ... buttons ... */}
</div>
);
};
// action.ts
"use server";
import { revalidatePath } from "next/cache";
export async function updateCartItem(productId: string, quantity: number) {
try {
await api.patch(`/cart/${productId}`, { quantity });
// This runs successfully on the server
revalidatePath("/cart");
} catch (error) {
console.error("Error updating item in cart:", error);
}
}
// logs from the backend
api-gateway:dev: PATCH /api/cart/id... 200 15ms
api-gateway:dev: GET /api/cart/count 200 8ms
api-gateway:dev: GET /api/cart 200 11msWhat happens: I click the generic +/- button, startTransition triggers the Server Action, the backend succeeds. My API gateway logs show the PATCH was successful and the subsequent GET requests (triggered by revalidation) return 200 OK with the new data. But on the frontend, the pending state from useTransition remains true and the UI never updates with the new quantity. This issue seems specific to aggressive production caching. It works perfectly in development but fails intermittently in production builds. What I tried that did not fix it:
Nothing breaks the pending state or forces the UI to render the new server value when cacheComponents is enabled. Has anyone seen useTransition hang specifically when component caching is enabled? Is there a proper way to bust the client-side router cache in this setup? I’ve been stuck on this since yesterday morning and could really use some insight. output.mp4Additional informationNo response ExampleNo response |
Replies: 4 comments 23 replies
|
Hi @tomarrohitt Client Component "use client";
import { updateCartItem } from "@/actions/cart";
import { useTransition } from "react";
export const CartQuantity = ({ item }) => {
const [pending, startTransition] = useTransition();
const handleQuantityChange = (quantity: number) => {
if (pending) return;
startTransition(async () => {
await updateCartItem(item.productId, item.quantity + quantity);
});
};
return (
<span>
{pending ? <Loader2 className="animate-spin" /> : item.quantity}
</span>
);
};Server Action "use server";
import { revalidatePath } from "next/cache";
export async function updateCartItem(productId: string, quantity: number) {
await api.patch(`/cart/${productId}`, { quantity });
revalidatePath("/cart");
}startTransition(async () => {
await serverAction();
});✅ Safer pattern setLoading(true);
await serverAction();
setLoading(false); |
|
Hey @tomarrohitt |
|
For anyone else pulling their hair out over this, it turns out this is not an issue with your code,revalidatePath, or the Next.js Cache configuration. The Root Cause: This is a binary-level race condition in the React 19 Fiber Reconciler (specifically regarding resolveLazy and useId replay tracking). When a Server Action resolves very quickly, the reconciler marks the boundary as suspended but fails to "replay" the update to the DOM, leaving the UI stuck in a pending state even though the data has arrived on the client. The issue was introduced in Next.js commit that upgraded React from eaee5308-20250728 to 9be531cd-20250729. Which was fixed in a React Patch on Jan 17, 2026. This is tracked in commit that created the issue in NextJs e5c1dff8262b4d7dcef5bda0af9d9171196457bd The Fixes:
Option 2: What I did Since next@canary can be unstable for production, the safest path is to remove the buggy React 19 engine entirely. I downgraded to Next.js 15.3.6 and react 19.0.0 Security Note: If you downgrade, make sure to use next@15 (not an old specific patch) to ensure you are covered against CVE-2025-66478. TL;DR: It's a React 19 bug . Upgrade to the absolute latest Canary or downgrade to React 19.0.0 (Next.js 15 or Next.js 14) to solve it. Don't waste time debugging your revalidatePath logic. |
|
Data point as requested upthread: we reproduce this exact symptom (form action via Full details, statistics from repeated-fresh-build sampling, and what we ruled out: #97990 — happy to run instrumented builds and share Playwright traces. |
For anyone else pulling their hair out over this, it turns out this is not an issue with your code,revalidatePath, or the Next.js Cache configuration.
The Root Cause: This is a binary-level race condition in the React 19 Fiber Reconciler (specifically regarding resolveLazy and useId replay tracking). When a Server Action resolves very quickly, the reconciler marks the boundary as suspended but fails to "replay" the update to the DOM, leaving the UI stuck in a pending state even though the data has arrived on the client.
The issue was introduced in Next.js commit that upgraded React from eaee5308-20250728 to 9be531cd-20250729. Which was fixed in a React Patch on Jan 17, 2026.
#86055
react/…