diff --git a/.changeset/partial-match-key-undefined.md b/.changeset/partial-match-key-undefined.md new file mode 100644 index 00000000000..a79b6cd6003 --- /dev/null +++ b/.changeset/partial-match-key-undefined.md @@ -0,0 +1,5 @@ +--- +'@tanstack/query-core': patch +--- + +fix(query-core): ignore `undefined` properties in partial-match filter keys so that `partialMatchKey` is consistent with the default `hashKey`, which drops them. Keys like `['todos', { status: undefined }]` now partially match queries with a concrete value for that property, e.g. `['todos', { status: 'open' }]` diff --git a/packages/query-core/src/__tests__/queryClient.test.tsx b/packages/query-core/src/__tests__/queryClient.test.tsx index f68670104a2..334ee38cd18 100644 --- a/packages/query-core/src/__tests__/queryClient.test.tsx +++ b/packages/query-core/src/__tests__/queryClient.test.tsx @@ -2547,6 +2547,30 @@ describe('queryClient', () => { expect(queryFn).toHaveBeenCalledTimes(1) unsubscribe() }) + + it('should match queries when a filter key property is undefined but the query key has a concrete value', async () => { + const key = queryKey() + await queryClient.fetchQuery({ + queryKey: [key, { status: 'open' }], + queryFn: () => Promise.resolve('open'), + }) + await queryClient.fetchQuery({ + queryKey: [key, { assignee: 'a' }], + queryFn: () => Promise.resolve('assigned'), + }) + + await queryClient.invalidateQueries({ + queryKey: [key, { status: undefined }], + refetchType: 'none', + }) + + expect( + queryClient.getQueryState([key, { status: 'open' }])?.isInvalidated, + ).toBe(true) + expect( + queryClient.getQueryState([key, { assignee: 'a' }])?.isInvalidated, + ).toBe(false) + }) }) describe('resetQueries', () => { diff --git a/packages/query-core/src/__tests__/utils.test.tsx b/packages/query-core/src/__tests__/utils.test.tsx index c9566ddd376..042171f51a7 100644 --- a/packages/query-core/src/__tests__/utils.test.tsx +++ b/packages/query-core/src/__tests__/utils.test.tsx @@ -179,6 +179,27 @@ describe('core/utils', () => { partialMatchKey(queryKeyWithUndefined, queryKeyWithoutProperty), ).toBe(true) }) + + it('should ignore undefined filter properties when matching concrete values', () => { + const a = ['todos', { page: 1, status: 'open' }] + const b = ['todos', { page: 1, status: undefined }] + + expect(partialMatchKey(a, b)).toBe(true) + }) + + it('should ignore undefined filter properties recursively', () => { + const a = ['todos', { filters: { status: 'open', assignee: 'a' } }] + const b = ['todos', { filters: { status: undefined } }] + + expect(partialMatchKey(a, b)).toBe(true) + }) + + it('should not match a concrete filter value against an undefined query key property', () => { + const a = ['todos', { status: undefined }] + const b = ['todos', { status: 'open' }] + + expect(partialMatchKey(a, b)).toBe(false) + }) }) describe('replaceEqualDeep', () => { diff --git a/packages/query-core/src/utils.ts b/packages/query-core/src/utils.ts index f442ab86fdc..e1e72468b24 100644 --- a/packages/query-core/src/utils.ts +++ b/packages/query-core/src/utils.ts @@ -267,6 +267,9 @@ export function partialMatchKey(a: any, b: any): boolean { const bKeys = Object.keys(b) for (const key of bKeys) { + if (b[key] === undefined) { + continue + } if (!partialMatchKey(a[key], b[key])) { return false }