There is a correctness bug in useOptimistic where the "revert" mechanism is gated by a global (per-root) async action reference count rather than being scoped to the specific action or transition that triggered it.
In an application where two unrelated components trigger async actions, a "Slow" action in Component A will prevent the optimistic UI in Component B from reverting or finalizing, even after Component B's action has fully resolved or failed.
The Root Cause
The issue is located in ReactFiberAsyncAction.js. The implementation currently uses module-level variables to track the state of "entangled" actions:
// From ReactFiberAsyncAction.js
let currentEntangledPendingCount: number = 0;
let currentEntangledListeners: Array<() => mixed> | null = null;
As noted in the source comments, React currently entangles all concurrent async actions because it lacks a mechanism like AsyncContext to distinguish between them. While this entanglement might be an intentional trade-off for scheduling, it breaks the functional requirements of useOptimistic. Specifically, pingEngtangledActionScope only notifies listeners (which useOptimistic relies on) when the global currentEntangledPendingCount returns to zero
Reproduction
I have verified this behavior in a Vite-based React environment.
https://codesandbox.io/p/sandbox/frosty-https-d46684
There is a correctness bug in useOptimistic where the "revert" mechanism is gated by a global (per-root) async action reference count rather than being scoped to the specific action or transition that triggered it.
In an application where two unrelated components trigger async actions, a "Slow" action in Component A will prevent the optimistic UI in Component B from reverting or finalizing, even after Component B's action has fully resolved or failed.
The Root Cause
The issue is located in ReactFiberAsyncAction.js. The implementation currently uses module-level variables to track the state of "entangled" actions:
// From ReactFiberAsyncAction.js
let currentEntangledPendingCount: number = 0;
let currentEntangledListeners: Array<() => mixed> | null = null;
As noted in the source comments, React currently entangles all concurrent async actions because it lacks a mechanism like AsyncContext to distinguish between them. While this entanglement might be an intentional trade-off for scheduling, it breaks the functional requirements of useOptimistic. Specifically, pingEngtangledActionScope only notifies listeners (which useOptimistic relies on) when the global currentEntangledPendingCount returns to zero
Reproduction
I have verified this behavior in a Vite-based React environment.
https://codesandbox.io/p/sandbox/frosty-https-d46684