When caching data that arrives asynchronously (API/desktop invokes, polling, or snapshot-driven loads), make cache coherence explicit so late/stale responses can’t overwrite fresher state.
Apply these rules: 1) Version per cache key (generation/token)
2) Handle in-flight reads deterministically
3) Unify refresh paths
4) Cache stable configuration with correct lifecycle scope
Minimal pattern (per-key generation + discard late writes):
const readGenRef = useRef(new Map<string, number>());
const invalidateReads = (keys: Iterable<string>) => {
for (const key of keys) {
readGenRef.current.set(key, (readGenRef.current.get(key) ?? 0) + 1);
}
};
async function readKey(key: string) {
const gen = readGenRef.current.get(key) ?? 0;
const result = await fetchSomethingFor(key);
if ((readGenRef.current.get(key) ?? 0) !== gen) return; // discard stale
setCacheFor(key, result);
}
Also add tests that reproduce the failure mode: keep a read in flight, trigger refresh/invalidation, then ensure the late response does not overwrite the fresh contents and that any required follow-up read happens exactly once.