[React 19] useOptimistic shows wrong value when other actions happen in the background
Summary
Any unrelated background action blocks useOptimistic’s heuristic as to when to revert to the real value.
Reproduce
-
Open the demo
Code backup
import { useOptimistic, useState } from "react"; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); export default function App() { const [a, setA] = useState(0); const [optimisticA, setOptimisticA] = useOptimistic(a); async function incA() { const newVal = optimisticA + 1; setOptimisticA(newVal); await sleep(1000); return Error("failed"); } async function unrelatedSlow() { await sleep(20000); } return ( <div className="App"> <div> <form action={incA}> {optimisticA} <button disabled={optimisticA !== a}>Inc</button> </form> </div> <div> <form action={unrelatedSlow}> <button>Unrelated slow</button>{" "} </form> </div> </div> ); } -
Click on Inc.
-
Observe how after 1 second and a simulated server failure the number goes back to 0.
-
Click on Unrelated slow.
-
Click on Inc.
Expected: after 1 second number goes back to 0, same as before. Actual: the number stays at 1 for 20 seconds, until the unrelated action completes.
Personally, I am not sure how the current design of useOptimistic should be able to work.
In the general case, when building optimistic UI, every change may need to be rebased or reverted. The only way to achieve that is for the changes to be expressed as pure functions. (Perhaps the second parameter to useOptimistic is supposed to be required, and then #30638 is the answer to my question)
Also, in the general case when building optimistic UI, every change must be tied to the success and failure. As in we need to know which change to rollback and which to replay. The current design seems to be fundamentally unable to do that, and thus more complex state changes might never play well with concurrent operations. (Perhaps this is the cause of #28574)
This is not a theoretical concern. Multiple concurrent optimistic operations are absolutely the norm in a complex local-first application.
@denis-sokolov I can work on this issue can you please assign me this issue
@denis-sokolov I'd like to help with this issue. Could you please assign it to me?
- Click on Inc.
- Observe how after 1 second and a simulated server failure the number goes back to 0.
- Click on Unrelated slow.
- Click on Inc.
Don't you think it's strange that even if you don't return an error object, it still returns a value? I forked your example, removed the error object and it still returns 0 even without an error
I may not understand something and this api is new to me, but it shouldn't work like this https://github.com/facebook/react/issues/31967
This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!
Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please create a new issue with up-to-date information. Thank you!
Updated with the newest React version and issue details and reopened as #33545.