query icon indicating copy to clipboard operation
query copied to clipboard

Is React Query incompatible with React Actions/Transitions/`useOptimistic`?

Open OliverJAsh opened this issue 4 months ago • 1 comments

In this reduced test case I'm seeing some odd behaviour when combining React Query with React Actions/Transitions and useOptimistic.

  • We have a query that returns a count, starting at 1.
  • When a user clicks this button, we run an action to add 1 to the count. The action does the following:
    1. optimistically adds 1 to the count
    2. server mutation to update the server-side count
    3. refetch query for count

If we click the button once, I expect the count to show 2.

However, the actual behaviour is this:

  1. Count shows 2 (optimistic update).
  2. Then count briefly shows 3 when the query refetch finishes.
  3. Then count reverts to 2.

https://github.com/user-attachments/assets/6a60e351-fb76-4162-8b60-48adcca9f518

My investigation

Looking at the logs, I believe this is what is happening:

  1. Server state begins at 1 ✅
  2. User clicks button. Optimistic count is now server state + 1 = 2. ✅
  3. Refetch finishes. React Query synchronously updates. The new server state is 2. React rebases the optimistic state on top of the new server state: server state + 1 = 3. ❌
  4. Finally, action finishes so React drops optimistic state and count reverts back to 2. ✅
Image

If my hypothesis is correct, I believe this happens because React Query uses useSyncExternalStore. As per the docs:

If the store is mutated during a non-blocking Transition update, React will fall back to performing that update as blocking.

If that is the case then I guess we can't really expect React Query to work with actions/transitions/useOptimistic? Has this issue come up before and are there any plans to resolve this? 🙏

For comparison, the same test case but without React Query works with no such problems:

  1. Server state begins at 1 ✅
  2. User clicks button. Optimistic count is now server state + 1 = 2. ✅
  3. Refetch finishes. The new server state is 2 but the state update is part of the transition, so it doesn't happen synchronously. Optimistic count continues to show 2. ✅
  4. Finally, action finishes so React drops optimistic state and count reverts back to server state (2). ✅

Your minimal, reproducible example

https://codesandbox.io/p/sandbox/react-dev-forked-lhxlvx

TanStack Query version

5.90.2

OliverJAsh avatar Oct 09 '25 17:10 OliverJAsh

I think we need concurrent stores for this

TkDodo avatar Oct 16 '25 13:10 TkDodo