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.
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, projectcomplete: 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: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.