Skip to content

query-mutation-missing-invalidation: explicit Zustand reconciliation still reports stale data #1786

Description

@thomasorgeval

A mutation orchestrates a purchase, then fetches the current membership and updates the Zustand store consumed by the UI. There is no TanStack query caching membership in this example, but the rule still requires query invalidation.

Reproduction

Reproduced with React Doctor 0.9.13, Node 24.18.0, on macOS. JSON report: schemaVersion: 3, ok: true, project complete: true. This is a static-analysis reproduction; no runtime jank is inferred from the warning.

Create a directory with this package.json, add the file below, and run:

npx --yes react-doctor@0.9.13 . --scope full --json --blocking none --yes --no-score
{"name":"react-doctor-rule-repros","private":true,"version":"1.0.0","dependencies":{"react":"19.2.7","react-native":"0.86.2","@shopify/flash-list":"2.3.2","@tanstack/react-query":"5.101.0","zustand":"5.0.14"}}

Upgrade.tsx:

import { useMutation } from '@tanstack/react-query';
import { Button, Text, View } from 'react-native';
import { create } from 'zustand';

declare function purchase(): Promise<'purchased' | 'cancelled'>;
declare function getMembership(): Promise<{ tier: string }>;
const useMembership = create<{ tier: string }>(() => ({ tier: 'free' }));

async function reconcileMembership() {
  const membership = await getMembership();
  useMembership.setState(membership);
}

export function Upgrade() {
  const tier = useMembership((state) => state.tier);
  const upgrade = useMutation({
    mutationFn: async () => {
      if ((await purchase()) === 'purchased') await reconcileMembership();
    },
  });
  return (
    <View>
      <Text>{tier}</Text>
      <Button title="Upgrade" disabled={upgrade.isPending} onPress={() => upgrade.mutate()} />
    </View>
  );
}

Actual

“useMutation with no cache update here can leave your users looking at stale data after it runs.”

The recommendation adds queryClient.invalidateQueries(...). There is no affected query key to invalidate: the successful path already updates the actual data owner.

Expected

Recognize proven synchronization of the relevant state owner when possible, or classify this as an unconfirmed cache concern / support a narrowly scoped documented opt-out. Do not blanket-exempt all Zustand writes: an unrelated store update would not prove cached server data is fresh.

Notes

Related but distinct from #1759 / #1760: that fix handles magic-link delivery, which has no cached state change. Here state does change and is explicitly reconciled into a different store. The helper is in the same file to make that ownership visible in a minimal example.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions