- The symptom
- After rapid toggles and a failed request, the UI shows the wrong value.
- The cause
- Rollback restores a value captured before newer interactions happened.
- The fix
- Allow only the latest request to confirm or roll back.
Problem
An optimistic UI update immediately reflects a user action before the server confirms success. If the request fails or multiple toggles occur quickly, the UI can drift away from actual server state.
The failure becomes visible when responses resolve out of order. Because rollback logic depends on previously captured state, rapid interactions can cause the UI to revert to an outdated value that no longer represents user intent.
- User toggles state rapidly.
- UI updates immediately (optimistic update).
- Server responds out of order or fails.
- Rollback logic restores incorrect state.
1const [liked, setLiked] = useState(false);
2
3function handleToggle() {
4 const previous = liked;
5 setLiked(!liked); // optimistic update
6
7 void (async () => {
8 try {
9 await fakeRequest();
10 console.log("[BROKEN] server confirmed");
11 } catch {
12 console.log("[BROKEN] server failed → rolling back");
13 setLiked(previous);
14 }
15 })();
16}Because the stored 'previous' value represents state at a specific moment in time, it may no longer be valid by the time the request resolves. Rollback logic based on stale baselines introduces subtle state corruption.
Solution
Each optimistic mutation is assigned a request identity. Only the latest request is allowed to commit success or rollback. Outdated responses are ignored instead of mutating state.
Instead of relying on a captured 'previous' value, the UI validates whether the response still matches the most recent user intent. This ensures rollback only occurs when it reflects the current interaction context.
1const requestIdRef = useRef(0);
2const [liked, setLiked] = useState(false);
3
4function handleToggle() {
5 const requestId = ++requestIdRef.current;
6 setLiked((prev) => !prev);
7
8 void (async () => {
9 try {
10 await fakeRequest();
11
12 if (requestId !== requestIdRef.current) {
13 console.log("[FIXED] stale response ignored");
14 return;
15 }
16
17 console.log("[FIXED] server confirmed");
18 } catch {
19 if (requestId !== requestIdRef.current) {
20 console.log("[FIXED] stale failure ignored");
21 return;
22 }
23
24 console.log("[FIXED] server failed → reverting safely");
25 setLiked((prev) => !prev);
26 }
27 })();
28}By validating request identity before committing success or rollback, the UI remains aligned with the latest user intent even under rapid interaction and network variance.
Check your own code
- 01Is rollback based on a value captured before the latest interaction?
- 02What should the UI show if an older request fails?
- 03Have you tested rapid toggling against a failing endpoint?
- optimistic ui
- frontend edge case
- state rollback bug
- react async failure
- ui correctness

Written by Sumeet Kumar Paul
Senior Frontend Engineer from India. Frontend Lab is where I reproduce real interface bugs and work through the fix.
Need a hand?
I build frontends like this for a living.
More from the lab
Sumeet Paul· 2 min readState update after route transition
A component starts asynchronous work and then a route transition occurs before the work completes. When the async task resolves, it updates state that no longer matches the active route.
Sumeet Paul· 2 min readDouble submit caused by UI state lag
A submit button is disabled only after state updates to 'loading'. Under rapid user interaction, multiple clicks can occur before the UI visually reflects the disabled state.
Sumeet Paul· 2 min readStale closure in click outside listener
Event listeners defined inside effects capture state from the render during which they were created. If the listener is attached once and depends on state that later changes, it may continue operating on outdated values.