DEV Community

Cover image for React 19's useOptimistic Fixed My Instant UI. Then Combining It With useActionState Broke My Reset Button

React 19's useOptimistic Fixed My Instant UI. Then Combining It With useActionState Broke My Reset Button

Shubhra Pokhariya on August 04, 2026

Part 1 was about waiting well. This one is about not waiting at all, and about a couple of mistakes I made along the way that are worth walking thr...
Collapse
 
webdeveloperhyper profile image
Web Developer Hyper

Good debugging post as usual! 😀 Small differences in how functions behave can be surprisingly hard to learn. Following your last post about useActionState, this time I got to learn about useOptimistic. Thank you!

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Thanks so much! 😊 It really means a lot that you've been following the series.

Those small behavioral differences are exactly the kind of things I wanted to focus on because they're easy to miss until they show up in a real app. I'm glad you found it useful!

Collapse
 
vinimabreu profile image
Vinicius Pereira

The three mistakes have one root worth naming: optimistic state is derived, not owned. Forgetting onConfirmed is failing to update the source it derives from, the manual reset is writing to a projection, and generating an id inside the updater assumes it runs once like an event handler rather than as a derivation React is free to re-run. If you catch yourself resetting it, writing to it, or minting values in it, you have started treating a view as a store.

One practical follow-on to the temp id: have the server accept the client-generated uuid instead of assigning its own. Then the optimistic row and the confirmed row share a key, the swap reconciles invisibly instead of flashing a duplicate, and you get idempotency on retry for free, since a resubmitted comment with the same id is a no-op rather than a second comment.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Thanks, Vinicius! I really like the way you framed it as "derived, not owned." That ties the three mistakes together really well.

The client-generated UUID suggestion is a great extension too. Letting the server accept it keeps the identity consistent all the way through, and the idempotency benefit on retries is a nice bonus.

Collapse
 
vinimabreu profile image
Vinicius Pereira

Happy it was useful. One edge on the client-generated id: the server has to make the insert idempotent too, not just accept the id. INSERT ... ON CONFLICT (id) DO NOTHING plus a read-back, so a retry returns the original row instead of a unique violation the user reads as a real error. And keep identity the only thing the client owns. Timestamps and author stay server-assigned.

Also worth generating it as UUIDv7 rather than v4. v7 is time-ordered, so the optimistic row sorts into its final position right away. With v4 you fall back to sorting by timestamp, and the client and server clocks disagree just enough that the row can jump when the confirmed version lands. crypto.randomUUID() gives you v4, so v7 needs a small helper.

Thread Thread
 
shubhradev profile image
Shubhra Pokhariya

Exactly. That completes the picture nicely. Keeping the client-generated ID stable solves the identity side, but the backend still has to make retries idempotent for the whole flow to hold together.

I also hadn't considered the UUIDv7 ordering angle. That's a really elegant way to keep the optimistic and confirmed states aligned.

"Derived, not owned" still feels like the thread that ties all of this together. Thanks for expanding on it, Vinicius!

Thread Thread
 
vinimabreu profile image
Vinicius Pereira

Glad it landed. Good post to think out loud in.

Collapse
 
muhammad_lutfimuzaki_ profile image
Muhammad Lutfi Muzaki

This is an excellent point by Vinicius. Treating optimistic state as a derived projection rather than a source of truth is a key mindset shift. Another challenge is managing race conditions when multiple actions are in flight - e.g., if you submit comment A, then comment B before A completes. useOptimistic handles this internally by queueing updates, but if the server response for A fails and B succeeds, reconciling that state back to the 'true' state from useActionState requires careful state machine design. Client-generated UUIDs definitely simplify the reconciliation here.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Thanks, that's a good edge case to raise. One small distinction though: the queueing here is actually coming from useActionState's dispatchAction, not useOptimistic. I covered that in the AbortController section: dispatches are processed in the order they arrive rather than racing each other.

In this particular CommentBox, the A-then-B scenario also can't happen through the UI because the input and submit button are disabled while isPending is true. And since both submissions would go through the same formAction, useActionState would process them sequentially anyway.

So there's no A-fails/B-succeeds reconciliation race in this specific example. The broader concern is definitely relevant for optimistic updates that involve multiple independent actions, where nothing is serializing the requests for you. That's where the reconciliation and state-machine complexity becomes much more interesting.

And yes, the client-generated ID still helps with identity and retry reconciliation, as Vinicius pointed out earlier.

Collapse
 
glenallen profile image
Glen Allen

Great breakdown. At IT Path Solutions, we've seen that optimistic UI patterns deliver the best experience when they're paired with careful state transition testing. Most production issues tend to appear where multiple hooks and async workflows intersect, making end-to-end validation just as important as individual feature testing.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Thanks, Glen! I agree. Neither hook was the problem on its own. It was the interaction between them that exposed the edge case, and that was the part I found most interesting to debug.

Collapse
 
mudassirworks profile image
Mudassir Khan

"optimistic state is derived, not owned" is the mental model I wish I'd had earlier. the trap we hit was treating the optimistic setter like a manual state toggle and expecting it to survive form resets. it doesn't — once the transition settles, the value prop overwrites it, which is exactly correct behavior but feels like a bug when you're not expecting it.

the manual rollback pattern is gnarly at scale too. once you have more than two in flight optimistic updates at once, the rollback order starts mattering and things get messy fast.

how are you handling the case where useActionState's reset and useOptimistic's revert disagree on timing?

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Exactly, Mudassir. That's how I think about it now. In this particular example, I avoid the timing conflict rather than trying to coordinate two resets.

Reset is disabled while isPending is true, so by the time it can actually run, the Transition has already settled and optimisticComments is already back to comments. There's nothing left for useOptimistic to reset at that point. formAction(null) only clears the useActionState result, which is the one piece of state that was actually still holding onto something.

If the two ever did need to reset at genuinely different moments, I'd rather model that as two explicit signals than add a second reset path into useOptimistic and end up fighting a lifecycle it's already managing correctly on its own.

And yeah, agreed on the manual rollback problem. Past a couple of updates in flight, you're basically hand-building the reconciliation useOptimistic gives you for free. That's really where "derived, not owned" earns its keep, once you stop treating the optimistic setter like a second setState and let it just be a projection of comments.

Collapse
 
hemapriya_kanagala profile image
Hemapriya Kanagala

Shubhra, I don't know React deeply enough yet to say much on the technical side 😄 but I've been enjoying how you walk through the mistakes you ran into instead of just showing the final working version. It makes the debugging process interesting to follow 😀

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Thank you so much, Hemapriya! 😊 That really means a lot.

I'm glad it was still easy to follow even without a deep React background. That was one of my goals while writing it. I also enjoy walking through the mistakes because they're often where the real learning happens. Thanks for reading and for sharing that!

Collapse
 
frank_signorini profile image
Frank

Super interesting read! I'm curious if you considered using a key prop on the form itself

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Thanks, Frank! I'm really glad you enjoyed the post. 😊

Good question. I did consider it, but a key on the <form> itself wouldn't affect useActionState or useOptimistic since those hooks live on CommentBox, not the form. The remount trade-off I described only comes into play if CommentBox itself is remounted.

Collapse
 
citedy profile image
Dmitry Sergeev

man useOptimistic is such a lifesaver, but yeah the state reset logic with useActionState is always a headache lol

Collapse
 
shubhradev profile image
Shubhra Pokhariya

That was my first impression too. In the end, I realized I only needed to reset one piece of state.

Collapse
 
citedy profile image
Dmitry Sergeev

did the same thing with useOptimistic last week and spent two hours debugging the state reset lol. definitely a weird quirk with how it interacts with actions.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Yeah, that debugging session is basically what pushed me to write this up. It feels like a quirk at first, but it's really just state living on the component that owns the hook. Once I traced where the state lived, the behavior stopped being surprising.

Collapse
 
elsie-rainee profile image
Elsie Rainee

Awesome post! 🔥 Loved how you explained everything so simply. Super helpful! 🙌

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Thank you so much Elsie! 😊 I'm really glad you enjoyed it. I always try to keep things as simple and practical as possible. Thanks for reading!

Collapse
 
scott_morrison_39a1124d85 profile image
Knowband

The section on AbortController and pending actions provides valuable context for handling edge cases.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Thanks! I'm glad that section was useful. Cancelling on the client doesn't undo a mutation that's already been processed by the server, so it's not a pattern that's safe for every action.

Collapse
 
hoseinmdev profile image
Hosein Mahmoudi

Really interesting write-up! I've actually just recently started hearing about these React 19 hooks, so this was super insightful. Thanks for sharing!

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Thanks, Hosein! 😊 I'm glad you found it useful. Thanks for reading!

Collapse
 
golen_0 profile image
Golen

Really good breakdown. The reset behavior with useOptimistic and useActionState is easy to get wrong. Thanks for sharing the real debugging experience.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Thanks so much! 😊 That was definitely one of the more interesting parts to debug. Once I understood what useOptimistic was actually doing there, the reset behavior stopped feeling like a quirk.