From 3a93ff0d2a6a0b30727d8efc259c9ea37d77fb16 Mon Sep 17 00:00:00 2001 From: yeshyungseok Date: Thu, 25 Jun 2026 10:11:07 +0900 Subject: [PATCH] fix(query-core): distinguish query keys differing only by a Date in partialMatchKey Non-exact key matching traversed objects structurally, so two query keys differing only by a `Date` value (e.g. `['report', { from: dateA }]` vs `['report', { from: dateB }]`) were treated as equal even though `hashKey` stores them as separate queries. This caused `invalidateQueries`, `refetchQueries`, `removeQueries`, and `cancelQueries` (which default to non-exact matching) to match unrelated sibling queries. `partialMatchKey` now compares non-plain objects the same way `hashKey` serializes them, keeping non-exact matching consistent with cache identity. Co-authored-by: Claude --- .changeset/spicy-dates-match.md | 7 ++++++ .../query-core/src/__tests__/utils.test.tsx | 22 +++++++++++++++++++ packages/query-core/src/utils.ts | 14 +++++++++++- 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 .changeset/spicy-dates-match.md diff --git a/.changeset/spicy-dates-match.md b/.changeset/spicy-dates-match.md new file mode 100644 index 00000000000..1f8e2929914 --- /dev/null +++ b/.changeset/spicy-dates-match.md @@ -0,0 +1,7 @@ +--- +'@tanstack/query-core': patch +--- + +fix(`partialMatchKey`): distinguish query keys that differ only by a `Date` + +Non-exact key matching (used by `invalidateQueries`, `refetchQueries`, `removeQueries`, `cancelQueries`, etc.) traversed objects structurally, so two keys differing only by a `Date` value (e.g. `['report', { from: dateA }]` vs `['report', { from: dateB }]`) were treated as equal even though `hashKey` stores them as separate queries. `partialMatchKey` now compares non-plain objects the same way `hashKey` serializes them, keeping non-exact matching consistent with cache identity. diff --git a/packages/query-core/src/__tests__/utils.test.tsx b/packages/query-core/src/__tests__/utils.test.tsx index 9abd1bf265c..dd9286c3e01 100644 --- a/packages/query-core/src/__tests__/utils.test.tsx +++ b/packages/query-core/src/__tests__/utils.test.tsx @@ -166,6 +166,28 @@ describe('core/utils', () => { const b = [{ a: null, c: 'c', d: [{ d: 'd ' }] }] expect(partialMatchKey(a, b)).toEqual(false) }) + + it('should distinguish different `Date` values, consistent with `hashKey`', () => { + expect(partialMatchKey([new Date(0)], [new Date(1000)])).toEqual(false) + expect( + partialMatchKey( + ['report', { from: new Date(0) }], + ['report', { from: new Date(1000) }], + ), + ).toEqual(false) + }) + + it('should return `true` for equal `Date` values', () => { + expect(partialMatchKey([new Date(0)], [new Date(0)])).toEqual(true) + }) + + it('should match non-plain objects that hash equally (e.g. `Map`)', () => { + // `Map`/`Set` serialize to `{}` via `hashKey`, so they share a cache + // entry and must keep matching, unlike `Date`. + expect( + partialMatchKey([new Map([['a', 1]])], [new Map([['a', 2]])]), + ).toEqual(true) + }) }) describe('replaceEqualDeep', () => { diff --git a/packages/query-core/src/utils.ts b/packages/query-core/src/utils.ts index b97b2cc5a33..45df8753aef 100644 --- a/packages/query-core/src/utils.ts +++ b/packages/query-core/src/utils.ts @@ -256,7 +256,19 @@ export function partialMatchKey(a: any, b: any): boolean { } if (a && b && typeof a === 'object' && typeof b === 'object') { - return Object.keys(b).every((key) => partialMatchKey(a[key], b[key])) + // Plain objects and arrays are matched structurally. Other objects + // (e.g. `Date`) are compared the same way `hashKey` serializes them, so + // that non-exact matching stays consistent with how queries are stored. + if ( + (isPlainArray(a) && isPlainArray(b)) || + (isPlainObject(a) && isPlainObject(b)) + ) { + return Object.keys(b).every((key) => + partialMatchKey((a as any)[key], (b as any)[key]), + ) + } + + return hashKey([a]) === hashKey([b]) } return false