Ask
28

Optimistic update makes the UI flicker through a wrong state when the request fails — what is the correct rollback?

Added optimistic updates to a toggle. Happy path is lovely. The failure path is worse than what I had before.

What happens when the request fails: the toggle flips back, then briefly flips again, then settles. Sometimes it settles on the wrong value and stays there until I navigate away and come back.

I am writing to the cache in the mutation and rolling back in the error handler by writing the old value back. That seems to be roughly what the docs show and it clearly is not enough.

I suspect the problem is that a refetch is landing somewhere in the middle, but I cannot work out the ordering. What is the pattern that actually gets this right?

2 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @rollback_reference · 2h ago

    Your suspicion is right, and there are two separate bugs producing the two symptoms.

    The flicker is an in-flight refetch landing on top of your optimistic write. You write the optimistic value; a query that was already running finishes and writes the server's old value; your error handler then writes your snapshot. Three writes, and the middle one is not yours.

    The fix is to cancel outgoing queries for that key before you write optimistically. Every good implementation of this starts with that step and it is the one people skip because the happy path works without it.

    The wrong final value is your rollback writing a stale snapshot. If you captured the old value when the mutation was defined rather than immediately before the write, or if two mutations overlap, the value you restore is not the value that was there.

    So the correct sequence, in order, and the order is the whole answer:

    1. Cancel in-flight queries for the affected key
    2. Read the current cached value and keep it as the rollback snapshot
    3. Write the optimistic value
    4. On error, write the snapshot back
    5. On settled — success or error — invalidate the key so the server has the last word

    Step 5 is what fixes the state that persists until you navigate away. Without it, a failed rollback is never corrected by anything.

    30
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @server_is_truth · 2h ago

    The principle underneath the sequence above, which makes it easy to remember: the optimistic value is a guess with a deadline, and the server response ends it either way.

    A lot of the mess comes from treating the mutation's own response as the new truth and writing it into the cache instead of refetching. That works until the server does something you did not model — a computed field, a timestamp, a side effect on a related record — and now your cache is a plausible fiction. Invalidating and letting the query refetch is slightly slower and always correct.

    Two places this shows up:

    Lists. Optimistically adding a row means inventing an id. When the server responds with the real one, you either reconcile carefully or you refetch. Refetch, unless you have measured that you cannot afford to.

    Anything with ordering or aggregation. A toggle that changes a count somewhere else is exactly where hand-maintained cache updates go wrong, because you have to remember every derived thing.

    The rule I use: optimistically update for feel, invalidate for truth. The optimistic write buys you the instant response; the invalidation makes sure that after a second, what is on screen is what is in the database. If those two ever disagree permanently, you have a bug you cannot see.

    25
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report