DEV Community

Cover image for My Next.js 16 Optimistic UI Looked Perfect. Then Someone Clicked It Five Times Fast

My Next.js 16 Optimistic UI Looked Perfect. Then Someone Clicked It Five Times Fast

Shubhra Pokhariya on July 08, 2026

Everything worked. I'd wired up useOptimistic on a task list, the checkbox flipped the instant you clicked it, no spinner, no half-second lag, exac...
Collapse
 
hemapriya_kanagala profile image
Hemapriya Kanagala

I'm not a React/Next.js developer, but the lesson here applies everywhere. It's easy to test the "happy path" and forget what happens when users click faster than expected. Nice reminder that real users don't always behave like our demos 😄

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Thanks! Yeah, that gap between "works in my demo" and "works" is such an easy one to miss until a real user closes it for you. Appreciate you reading it, glad the lesson traveled outside React!

Collapse
 
webdeveloperhyper profile image
Web Developer Hyper

Great debugging post as always! 😀 Users sometimes use an app in ways we never expect, so it's hard to prevent every bug. 😅 I'd like to think about as many possible user behaviors as I can when building apps.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Thanks, happy to hear that! And yeah, that's pretty much the job. Half of debugging is just thinking, "okay, but what if someone does this weird thing?" before it actually happens to you. The five-click thing felt so dumb in retrospect too. Like, of course someone's gonna do that, but it never occurs to you until it happens.

Collapse
 
nazar-boyko profile image
Nazar Boyko

Scoping the pendingId lock to the row instead of the whole list is exactly right, and it's the part people get wrong first. One thing that might bite later: disabling only stops clicks that go through the checkbox, so if the same toggle can also fire from a keyboard shortcut or a re-render somewhere else, the guard has a gap. Keying the pending state off the item id (which you're already doing) rather than a plain boolean is what saves you there. Honestly the bit I hadn't thought about clearly before was your point that the auto-revert already handles rollback for a plain toggle, since most examples flip it back by hand and never explain why that's redundant.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Nazar, really good catch on the keyboard shortcut gap. Hadn't even thought about that while writing this. You're right, it's the id-keyed state doing the real work there, not the disabled attribute. The checkbox just happened to be the only path I was testing against. If a shortcut or something else could fire the same toggle, checking pendingId at the top of handleToggle would still catch it, even with the input not disabled. Not a real gap in this specific app since a checkbox is the only thing that can toggle a checkbox, but definitely worth flagging for anyone extending this into other trigger paths.

And that rollback point took me a minute to actually convince myself of too. I went in assuming I'd need the manual flip-back, same as every example does. It wasn't until I traced through what useOptimistic actually does on settle that I saw the manual version was just doing twice the work for nothing.

Collapse
 
alexshev profile image
Alex Shev

Optimistic UI always looks clean in the single-click demo. The real test is repeated intent, delayed responses, and partial failure. I like treating every optimistic action as a tiny distributed system: local state, server truth, replay, and reconciliation.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Alex, I like the "tiny distributed system" way of framing it. Repeated intent was exactly what the five-click test exposed, and looking back it maps pretty well to what I ended up debugging. Appreciate you reading.

Collapse
 
alexshev profile image
Alex Shev

Yes, that five-click test is the perfect example. Optimistic UI is not really a UI trick once users can repeat intent quickly; it becomes a reconciliation problem. I like testing it with duplicate clicks, delayed responses, and one forced failure before trusting the happy path.

Collapse
 
mudassirworks profile image
Mudassir Khan

the unstable_retry vs reset distinction is the one that bites first. we spent a sprint on a broken error screen — it was reset, which just rerenders without a server round trip. took us too long to figure out reset and settle are not the same thing.

the place it matters: data fetch failures behind a stale ISR cache. reset surfaces the cached version. unstable_retry forces a fresh render. for a task checkbox fine either way. for a shared dashboard with a 5 minute cache, completely different call.

was the revalidateTag second argument a 16.1 addition or was it there earlier?

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Mudassir, really good extension of the reset vs unstable_retry point, the ISR case is exactly where it stops being cosmetic. reset re-showing the stale cached version instead of fetching is the kind of bug that looks fine in a demo and quietly lies to users on a real dashboard.

On the version question: it was there from 16.0, not a 16.1 addition. It's in the original Next.js 16 blog post and the beta announcement before that, so anyone on 16.0+ already needs the second argument, not just people who've updated further.

Collapse
 
divineuzor profile image
Divine Uzor

The real lesson here for me, is that UI polish and backend reliability aren't separate concerns. One user clicking too fast shouldn't put load on your infrastructure. That's a design problem, not just a UX problem. Simple lock prevents so much downstream chaos.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Divine, exactly. It's easy to file this under a UI bug when it's really a design problem. Five clicks becoming one request isn't just smoother for the user, it's one less write the database has to process. That simple lock protects the backend just as much as it improves the UI.

Collapse
 
frank_signorini profile image
Frank

How did you handle cases where the user clicks multiple times in quick succession with useOptimistic, was it a custom implementation? I'm following your work for more insights on Next.js optimizations.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Frank, good question. useOptimistic doesn't handle this on its own, it just manages the temporary value, nothing about whether a second request should fire. So the multi-click part is custom: a pendingId state tracking which row has a request in flight, and that row's checkbox stays disabled until it settles. Basically the same idea as disabling a submit button while a form's in flight, just scoped to one row instead of the whole form. Appreciate you following along, more Next.js stuff coming.

Collapse
 
publiflow profile image
PubliFlow

Dealing with rapid clicks on optimistic UI updates is a classic race condition headache. I usually end up implementing a debounce or a queueing mechanism on the client side to batch those rapid state changes before they hit the server mutation. It prevents the UI from flickering wildly when the server responses finally catch up to the queued actions. I actually had to solve a similar concurrency issue when wiring up the real-time subscriptions in our Next.js SaaS boilerplate, PubliFlow.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Good point. Queueing makes a lot of sense for workflows where every action needs to be preserved and processed in order, like real-time subscriptions.

For this case, I wanted the opposite. A task toggle is idempotent, so five rapid clicks should end up in the same state as one. The per-row pending lock keeps it to one mutation at a time for that item, avoids redundant writes, and keeps the optimistic UI and the server from drifting out of sync.

Collapse
 
alexshev profile image
Alex Shev

Optimistic UI is one of those features that feels finished until the user behaves like a user. Fast repeated actions expose whether the state model is actually coherent. The visual optimism is easy; the hard part is making rollback, dedupe, pending state, and server truth feel like one system.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

That's a great way to look at it, Alex.

I really like the idea of thinking about it as one system instead of a collection of separate pieces. Looking back, that's exactly what the five-click case exposed. The optimistic update alone wasn't the hard part, it was everything around it that had to stay in sync.

Collapse
 
alexshev profile image
Alex Shev

Exactly. Optimistic UI is less a UI trick and more a distributed-state problem wearing a friendly face. The five-click case is useful because it exposes whether the client, server, retry behavior, disabled state, and final reconciliation are actually one system or just several pieces hoping the happy path holds.

Collapse
 
james_nava_88d210faf4702a profile image
James Nava

Am so grateful to the people who referred me to this hacker called jbeespyhack ❊ @ ❊ gm a il c om, I got what I paid for in full after my contact with this hacker, I got a complete iPhone 16 phone hacked and also a Samsung phone hacked and they all came out successfully with no trace left.. jbeespyhack AT gMail . C0m you indeed did a good job for me thank you

Collapse
 
promise_wisdom_663ab0e9a7 profile image
Promise Wisdom

HI