ZyVOP Logo
Content That Connects
SeriesAI NewsCategoriesWrite for Us
ZyVOP Logo
Content That Connects

Empowering developers and creators with cutting-edge insights, comprehensive tutorials, and innovative solutions for the digital future.

Content

  • Tags
  • Badges
  • Write Article
  • Newsletter

Company

  • About Us
  • Write for Us
  • Contact

Connect

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • DMCA Policy
  • Code of Conduct

© 2026 ZyVOP. Crafted with care for the developer community.

Made with ❤️ by the ZyVOP team
All systems operational
HomeOptimistic UI: Updating Before the Server Says Yes

Optimistic UI: Updating Before the Server Says Yes

Maksym Kuzmitskyi (MaximusFT)
Maksym Kuzmitskyi (MaximusFT)Staff Frontend Engineer
July 31, 2026
5 min read
1 views
Optimistic UI: Updating Before the Server Says Yes
#react#Architecture#React Playbook

Optimistic UI: Updating Before the Server Says Yes

At the end of the last article I said that once you hold one coherent copy of server state and can invalidate it precisely, a door opens that used to be terrifying: updating the UI before the server confirms, and rolling back cleanly if it rejects. That's optimistic UI, and I want to be honest about what it actually is before we make it feel magic.

Optimistic UI is a lie. A small, well-intentioned, usually-correct lie — but a lie. You click the star, and the star fills in immediately, before the server has any idea you did anything. You're betting the request will succeed and showing the user the future you expect. Most of the time you're right and the app feels instant. The entire craft is in what happens the rare time you're wrong.

Why we bother lying at all

The alternative is honest and awful. User clicks, you show a spinner, you wait for the round trip, then you update. On a good connection that's 200-400ms of the UI sitting there doing nothing after a click that should have felt instant. On a train, it's two seconds of a spinning star. For a like button, a toggle, a reorder — high-frequency, low-stakes actions — that latency is the whole experience, and it makes a fast app feel slow.

So we front-run the server. The user's intent is almost always going to succeed, so we render success now and reconcile with reality when it arrives. The feeling is immediate; the correctness catches up a beat later.

Optimistic UI is a promise to the user: "I'll show you this now, and I guarantee I'll fix it if I was wrong." If you can't keep the second half, don't make the first.

The dangerous naive version

Here's the version I see most often, and it's the one that ships bugs:

// DON'T: optimism with no way back
function useToggleStar(id: number) {
  const [starred, setStarred] = useState(false);
  const toggle = async () => {
    setStarred((s) => !s);        // update immediately
    await api.setStar(id, !starred); // hope it works
  };
  return { starred, toggle };
}

Read what happens when api.setStar throws. The state already flipped. There's no snapshot, no catch, no rollback. The star stays filled, the server never recorded it, and the user walks away believing they starred something they didn't. This isn't a cosmetic bug — the UI is now actively lying with no plan to stop. Optimism without rollback isn't optimistic UI. It's just a race you're pretending you won.

The real shape: snapshot, apply, reconcile

A correct optimistic update always has the same four beats. Cancel anything in flight that could clobber you. Snapshot the current truth. Apply the optimistic change. Then, when the request settles, either keep it (success) or restore the snapshot (failure). Because this series uses TanStack Query, the cache is where the truth lives, and the mutation lifecycle gives you exactly these hooks:

const toggleStar = useMutation({
  mutationFn: (id: number) => api.toggleStar(id),

  onMutate: async (id) => {
    // 1. stop in-flight refetches so they can't overwrite our optimistic value
    await queryClient.cancelQueries({ queryKey: ['todo', id] });

    // 2. snapshot the current truth so we can roll back
    const previous = queryClient.getQueryData<Todo>(['todo', id]);

    // 3. apply the optimistic change to the cache
    queryClient.setQueryData<Todo>(['todo', id], (old) =>
      old ? { ...old, starred: !old.starred } : old,
    );

    // hand the snapshot to onError via context
    return { previous };
  },

  onError: (_err, id, context) => {
    // 4a. reality rejected us — restore exactly what we had
    if (context?.previous) {
      queryClient.setQueryData(['todo', id], context.previous);
    }
  },

  onSettled: (_data, _err, id) => {
    // 4b. reconcile with the server's version, success or failure
    queryClient.invalidateQueries({ queryKey: ['todo', id] });
  },
});

Every line earns its place. cancelQueries is the one people skip and then can't explain the flicker: if a background refetch is already in flight, it will land after your optimistic write and stomp it with stale data. Snapshotting into context is what makes rollback surgical — you restore the exact previous object, not a guess. And onSettled invalidating is the humility step: whatever we optimistically showed, the server's answer is the real one, so we refetch and let truth win. This is why the previous article's precise invalidation mattered — optimistic UI is built directly on top of it.

Reconciliation is not "did it work" — it's "what's true now"

The subtle part is the reconcile step, because the world is not just success-or-failure. Sometimes the request succeeds but the server's result differs from what you optimistically drew. You optimistically added an item at the top of a list; the server inserts it sorted, so it's actually third. You optimistically set a counter to 5; someone else's change already made it 7.

The rule that keeps you sane: the server is the source of truth, and reconciliation means adopting its version, not defending yours. Your optimistic value was a placeholder to make the UI feel alive, nothing more. When the real data arrives, you replace the placeholder — even if it means the item you just "added to the top" visibly jumps to its sorted position. That tiny correction is honest and fine. Fighting to keep your optimistic guess because it looks smoother is how you end up with a client that slowly drifts out of sync with the backend.

Where it belongs, and where the lie gets expensive

I reach for optimistic UI when the action is cheap, reversible, and frequent, and when being wrong is cheap to correct. Toggling a star, liking a post, checking a checkbox, reordering a list, adding a comment. If the request fails, the rollback is a small, understandable flicker, and the user retries. The upside — instant feel — is huge and constant; the downside is rare and mild.

I do not reach for it when a wrong guess is expensive or confusing. Payments are the obvious one: never optimistically show "Payment successful." Showing success for a charge that then fails is worse than any spinner. Same for anything irreversible, anything with legal or money weight, and anything where the rollback would be more jarring than the wait — imagine optimistically navigating someone into a screen that then vanishes because the action failed. In those cases the honest spinner is the right call. The question isn't "can I make this optimistic?" It's "if I'm wrong, is the correction cheap and clear?" When the answer is no, don't tell the lie.

The reframe

What optimistic UI really taught me is that a good client models intent and confirmed truth as two different things, and knows how to move between them. The optimistic value is intent rendered early. The server response is confirmed truth. Reconciliation is the bridge. Once you hold those as separate ideas, the whole pattern stops feeling like a hack and starts feeling like what it is: a disciplined way to decouple how fast the UI feels from how fast the network actually is.

And notice what the rollback in onError really was — a single-step reversal back to a snapshot you took a moment ago. That works beautifully for one action. But the moment users want to reverse a sequence of actions — undo the last five edits in an editor, then redo two of them — a lone snapshot isn't enough. You need a history of reversible changes, which is a different and genuinely interesting problem. That's undo/redo, and it's next.

If you've got an optimistic update in your app right now, do me a favor and check one thing: what happens on onError? If you can't point to the line that restores the previous value, you don't have optimistic UI — you have a lie with no exit. Tell me what you find.

Maksym Kuzmitskyi (MaximusFT)

Maksym Kuzmitskyi (MaximusFT)

Staff Frontend Engineer

Passionate developer sharing knowledge about modern web technologies and best practices.

Comments (0)

Login to post a comment.

Related Posts

Service Discovery with Eureka and Spring Cloud: A Production Hands-On Guide

Learn how to build production-grade service discovery with Eureka and Spring Cloud LoadBalancer. Includes real-world heartbeat tuning and memory optimization tips.

Read article

Monorepo Best Practices

I learned to love monorepos the hard way — by suffering through the alternative. At a large Canadian automotive company, we had a repository for basic UI compon...

Read article

React Performance Optimization

I joined a large Canadian automotive company as one of three senior frontend engineers. The site was public-facing — a product catalog where users browsed vehic...

Read article

Complex Forms Done Right

At Motorola Solutions I built a multi-step contract wizard where users selected products, services, additional options, and pricing tiers — dozens of fields tha...

Read article

Type-Safe React Architecture

I remember the exact moment I realized I couldn't go back. I had been writing TypeScript for a while, got used to it, and then joined a project that was pure Ja...

Read article