ZYVOPMulti-Platform Sync
SeriesAI NewsWhy ZyVOPJoin Discord
LoginGet Started
ZYVOPMulti-Platform Sync

The Developer Publishing Hub. Write once, publish everywhere, and make your work citation-ready with built-in SEO, AEO, and GEO discovery support. Zero reader paywalls.

Content

  • Categories
  • Tags
  • Badges
  • Leaderboard
  • Write Article
  • Newsletter

Company

  • About Us
  • Why ZyVOP
  • Changelog
  • Compare Platforms
  • Hashnode vs ZyVOP
  • DEV vs ZyVOP
  • Developer API & CLI
  • Author Handbook
  • Contact

Connect

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

ยฉ 2026 ZyVOP. Developer Publishing Hub.

Zero paywalls ยท Full content ownership
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, 2026Updated August 30, 2026
5 min read
Optimistic UI: Updating Before the Server Says Yes
#react#React Playbook#Architecture

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.

Comments (0)

Login to post a comment.

Maksym Kuzmitskyi (MaximusFT)
Maksym Kuzmitskyi (MaximusFT)

Staff Frontend Engineer

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

Subscribe to Maksym Kuzmitskyi (MaximusFT)'s Newsletter

Direct email dispatches when new stories are published. Zero algorithms.

More from Maksym Kuzmitskyi (MaximusFT)

View profile

You Only Get to See It From the Outside Once

The most valuable thing an experienced engineer brings to a new project isn't a skill โ€” it's a vantage point. For a short window, you can still see the thing from the outside, the way a user or a newcomer does. Six months in, that view is gone. And most teams burn it in the first week without noticing.

6 minAug 11

The Command Palette Is an Architecture, Not a Widget

The theming article ended on a move from how an app looks to how power users drive it. The command palette โ€” the Cmd+K menu that fuzzy-searches everything you c...

5 minAug 7

Code-Splitting Is a Boundary Decision, Not a Bundle Trick

The command palette article closed by promising the last piece of this look-and-feel stretch, and it's the one that looks the most like a solved problem: code-s...

6 minAug 7

You Probably Don't Need Multi-Agents

I work across six or seven repositories on one project โ€” a big hybrid thing, part microfrontend, part backend, several apps that all talk to each other. When I ...

6 minAug 7

Analytics Is Architecture: Stop Sprinkling track() Everywhere

This cluster has been about how the parts of an app communicate โ€” event buses for ambient signals, feature flags for toggling behavior. Both articles kept circl...

5 minJul 30