Ask
143
@film_fridge ·

invalidateQueries resolves but the table keeps the old row until i navigate away Cache Invalidation

Mutation updates a row, then await queryClient.invalidateQueries({ queryKey: ['orders'] }). The promise resolves, devtools shows the query going stale, a refetch fires, and the response in the network panel contains the new value. The table still renders the old number until I navigate away and back.

React 19.1, TanStack Query v5.59. The table is a plain component reading data off useQuery. No React.memo, no custom equality, nothing exotic that I can find. Where would you look first?

9 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @mulch_marta · 6d ago

    If the cache does turn out to hold the stale value, look at your optimistic update. The classic shape is:

    onMutate: async () => { const prev = qc.getQueryData(key); ...; return { prev } }
    onError: (e, v, ctx) => qc.setQueryData(key, ctx.prev)
    onSettled: () => qc.invalidateQueries({ queryKey: key })
    

    If anything in that chain runs after the refetch resolves - a rollback firing on a request that actually succeeded but returned a non-2xx body, a second in-flight mutation settling late - you get exactly this: fresh data on the wire, stale data in the cache, and no error anywhere. Log dataUpdatedAt on every write and the ordering becomes obvious.

    86
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @clickertrainbee · 6d ago

    Also check you only have one QueryClient. If it is constructed inside a component body rather than in a module or useState(() => new QueryClient()), you can end up with a client that gets replaced on re-render, and then you are invalidating a cache nothing is subscribed to any more. Rare with the app router templates, common in code that grew out of a tutorial.

    57
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @torque_spec · last wk. · 3 replies

    First, split the question in half, because you are guessing across two systems. Open devtools, click the query, and look at the cached data after the refetch lands.

    • Cache holds the new value and the UI shows the old one: this is a render problem, not a cache problem. Ninety per cent of the time it is mirrored state - somebody did const [rows, setRows] = useState(data) or an uncontrolled defaultValue on the cell, and the initialiser only ever runs once.
    • Cache holds the old value: then the refetch you are watching belongs to a different key than the one your component subscribes to, or something wrote the old value back after it landed.

    That one check saves an afternoon. My money is on the first, given "navigate away and back" fixes it - remounting is exactly what re-runs a useState initialiser.

    129
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @film_fridge · 5d ago

      Cache had the new value. There is a useState(() => data.rows) two components down that I did not write and had never looked at. Deleted it, table updates. Thank you for the split rather than a guess.

      45
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
    • @torque_spec · last wk.

      It is nearly always that. The tell is "works after navigation" - a cache bug does not care whether the component remounted, and a mirrored-state bug cares about nothing else.

      52
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @schema_drift_lu · 6d ago

    Long shot but I lost a day to it: a select that returns a new derived array is fine, but a select that returns a slice of the same object combined with structural sharing can hand you back a reference that is === the previous one when only a nested field changed and your renderer compares that field by reference. Logging data in the component and seeing the old value print is enough to rule it in or out in thirty seconds.

    41
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @bobbin_bind · last wk. · 2 replies

    invalidateQueries only marks things stale, it does not guarantee a refetch of anything currently rendered. Use refetchQueries instead and await that - invalidate is for background correctness, refetch is for "I need the screen to be right now".

    33
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @schema_drift_lu · 7d ago

      invalidateQueries does refetch active queries by default - that is precisely the difference between active and inactive ones, and OP can see the refetch happening in the network panel. Swapping in refetchQueries would change nothing here.

      25
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @sam_the_temp · 6d ago

    Whatever it turns out to be, write down the answer somewhere. This exact symptom has cost me two afternoons in two different codebases and both times it was a different cause.

    18
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report