From 94e5360a31963c591bd12f5931444ad934c4f768 Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:35:01 +0100 Subject: [PATCH 1/4] perf: use stable-hash for hashing keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Using `JSON.stringify` is much slower when we only need it to create a stable value hash. Instead, we can use `stable-hash`. This is added as a devDependency so it ends up in the bundle rather than being a production dependency. On my machine, some bench results: | Task name | Latency avg (ns) | Latency med (ns) | Throughput avg (ops/s) | Throughput med (ops/s) | Samples | | -- | -- | -- | -- | -- | -- | | 'hashKey dev' | '29.05 ± 0.10%' | '41.00 ± 1.00' | '27211238 ± 0.01%' | '24390244 ± 580720' | 17211565 | | 'hashKey prod' | '942.42 ± 3.32%' | '916.00 ± 1.00' | '1096146 ± 0.01%' | '1091703 ± 1191' | 530549 | --- packages/query-core/package.json | 3 +- .../src/__tests__/hydration.test.tsx | 15 +++++--- .../__tests__/infiniteQueryBehavior.test.tsx | 7 ++-- .../query-core/src/__tests__/query.test.tsx | 3 +- .../query-core/src/__tests__/utils.test.tsx | 12 +++---- packages/query-core/src/hydration.ts | 4 +-- packages/query-core/src/query.ts | 5 +-- packages/query-core/src/utils.ts | 34 ++++++++++++------- pnpm-lock.yaml | 8 +++++ 9 files changed, 55 insertions(+), 36 deletions(-) diff --git a/packages/query-core/package.json b/packages/query-core/package.json index 03652df3182..dd4a3486595 100644 --- a/packages/query-core/package.json +++ b/packages/query-core/package.json @@ -59,6 +59,7 @@ ], "devDependencies": { "@tanstack/query-test-utils": "workspace:*", - "npm-run-all2": "^5.0.0" + "npm-run-all2": "^5.0.0", + "stable-hash": "^0.0.6" } } diff --git a/packages/query-core/src/__tests__/hydration.test.tsx b/packages/query-core/src/__tests__/hydration.test.tsx index 389226bd5b6..6fc60465c7b 100644 --- a/packages/query-core/src/__tests__/hydration.test.tsx +++ b/packages/query-core/src/__tests__/hydration.test.tsx @@ -5,6 +5,7 @@ import { QueryCache } from '../queryCache' import { dehydrate, hydrate } from '../hydration' import { MutationCache } from '../mutationCache' import { executeMutation, mockOnlineManagerIsOnline } from './utils' +import { hashKey } from '../utils'; describe('dehydration and rehydration', () => { beforeEach(() => { @@ -633,6 +634,7 @@ describe('dehydration and rehydration', () => { it('should set the fetchStatus to idle when creating a query with dehydrate', async () => { const key = queryKey() + const keyHash = hashKey(key) const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) @@ -662,7 +664,7 @@ describe('dehydration and rehydration', () => { const dehydrated = dehydrate(queryClient) resolvePromise('string') expect( - dehydrated.queries.find((q) => q.queryHash === JSON.stringify(key))?.state + dehydrated.queries.find((q) => q.queryHash === keyHash)?.state .fetchStatus, ).toBe('fetching') const stringified = JSON.stringify(dehydrated) @@ -679,7 +681,9 @@ describe('dehydration and rehydration', () => { it('should dehydrate and hydrate meta for queries', async () => { const metaKey = queryKey() + const metaKeyHash = hashKey(metaKey) const noMetaKey = queryKey() + const noMetaKeyHash = hashKey(noMetaKey) const queryCache = new QueryCache() const queryClient = new QueryClient({ queryCache }) queryClient.prefetchQuery({ @@ -698,21 +702,21 @@ describe('dehydration and rehydration', () => { const dehydrated = dehydrate(queryClient) expect( - dehydrated.queries.find((q) => q.queryHash === JSON.stringify(metaKey)) + dehydrated.queries.find((q) => q.queryHash === metaKeyHash) ?.meta, ).toEqual({ some: 'meta', }) expect( - dehydrated.queries.find((q) => q.queryHash === JSON.stringify(noMetaKey)) + dehydrated.queries.find((q) => q.queryHash === noMetaKeyHash) ?.meta, ).toEqual(undefined) expect( Object.keys( dehydrated.queries.find( - (q) => q.queryHash === JSON.stringify(noMetaKey), + (q) => q.queryHash === noMetaKeyHash, )!, ), ).not.toEqual(expect.arrayContaining(['meta'])) @@ -798,6 +802,7 @@ describe('dehydration and rehydration', () => { it('should not change fetchStatus when updating a query with dehydrate', async () => { const key = queryKey() + const keyHash = hashKey(key) const queryClient = new QueryClient() const options = { @@ -811,7 +816,7 @@ describe('dehydration and rehydration', () => { const dehydrated = dehydrate(queryClient) expect( - dehydrated.queries.find((q) => q.queryHash === JSON.stringify(key))?.state + dehydrated.queries.find((q) => q.queryHash === keyHash)?.state .fetchStatus, ).toBe('idle') const stringified = JSON.stringify(dehydrated) diff --git a/packages/query-core/src/__tests__/infiniteQueryBehavior.test.tsx b/packages/query-core/src/__tests__/infiniteQueryBehavior.test.tsx index 1a4bf1bee30..fe20790eb2c 100644 --- a/packages/query-core/src/__tests__/infiniteQueryBehavior.test.tsx +++ b/packages/query-core/src/__tests__/infiniteQueryBehavior.test.tsx @@ -2,16 +2,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { queryKey, sleep } from '@tanstack/query-test-utils' import { CancelledError, InfiniteQueryObserver, QueryClient } from '..' import { infiniteQueryBehavior } from '../infiniteQueryBehavior' -import type { InfiniteData, InfiniteQueryObserverResult, QueryCache } from '..' +import type { InfiniteData, InfiniteQueryObserverResult } from '..' describe('InfiniteQueryBehavior', () => { let queryClient: QueryClient - let queryCache: QueryCache beforeEach(() => { vi.useFakeTimers() queryClient = new QueryClient() - queryCache = queryClient.getQueryCache() queryClient.mount() }) @@ -39,10 +37,9 @@ describe('InfiniteQueryBehavior', () => { }) await vi.advanceTimersByTimeAsync(0) - const query = queryCache.find({ queryKey: key })! expect(observerResult).toMatchObject({ isError: true, - error: new Error(`Missing queryFn: '${query.queryHash}'`), + error: new Error(`Missing queryFn: '${JSON.stringify(key)}'`), }) unsubscribe() diff --git a/packages/query-core/src/__tests__/query.test.tsx b/packages/query-core/src/__tests__/query.test.tsx index fdac03e3987..9e1403c9051 100644 --- a/packages/query-core/src/__tests__/query.test.tsx +++ b/packages/query-core/src/__tests__/query.test.tsx @@ -860,10 +860,9 @@ describe('query', () => { const unsubscribe = observer.subscribe(() => undefined) await vi.advanceTimersByTimeAsync(10) - const query = queryCache.find({ queryKey: key })! expect(observer.getCurrentResult()).toMatchObject({ status: 'error', - error: new Error(`Missing queryFn: '${query.queryHash}'`), + error: new Error(`Missing queryFn: '${JSON.stringify(key)}'`), }) unsubscribe() }) diff --git a/packages/query-core/src/__tests__/utils.test.tsx b/packages/query-core/src/__tests__/utils.test.tsx index c9566ddd376..112c90eaeab 100644 --- a/packages/query-core/src/__tests__/utils.test.tsx +++ b/packages/query-core/src/__tests__/utils.test.tsx @@ -20,7 +20,7 @@ import { skipToken, } from '../utils' import { Mutation } from '../mutation' -import type { QueryFunctionContext } from '..' +import type { QueryFunctionContext, QueryKey } from '..' describe('core/utils', () => { describe('hashQueryKeyByOptions', () => { @@ -543,9 +543,9 @@ describe('core/utils', () => { describe('hashKey', () => { it('should hash primitives correctly', () => { - expect(hashKey(['test'])).toEqual(JSON.stringify(['test'])) - expect(hashKey([123])).toEqual(JSON.stringify([123])) - expect(hashKey([null])).toEqual(JSON.stringify([null])) + expect(hashKey(['test'])).toEqual('@"test",') + expect(hashKey([123])).toEqual('@123,') + expect(hashKey([null])).toEqual('@null,') }) it('should hash objects with sorted keys consistently', () => { @@ -556,7 +556,7 @@ describe('core/utils', () => { const hash2 = hashKey(key2) expect(hash1).toEqual(hash2) - expect(hash1).toEqual(JSON.stringify([{ a: 1, b: 2 }])) + expect(hash1).toEqual('@#b:2,a:1,,') }) it('should hash arrays consistently', () => { @@ -641,7 +641,7 @@ describe('core/utils', () => { const resolved = ensureQueryFn({ queryFn: skipToken, - queryHash: '["skip"]', + queryKey: ['skip'] as QueryKey, }) expect(consoleErrorSpy).toHaveBeenCalledWith( diff --git a/packages/query-core/src/hydration.ts b/packages/query-core/src/hydration.ts index 976b5faafee..fc0acd96204 100644 --- a/packages/query-core/src/hydration.ts +++ b/packages/query-core/src/hydration.ts @@ -1,5 +1,5 @@ import { tryResolveSync } from './thenable' -import { noop } from './utils' +import { describeKey, noop } from './utils' import type { DefaultError, MutationKey, @@ -89,7 +89,7 @@ function dehydrateQuery( // If not in production, log original error before rejecting redacted error if (process.env.NODE_ENV !== 'production') { console.error( - `A query that was dehydrated as pending ended up rejecting. [${query.queryHash}]: ${error}; The error will be redacted in production builds`, + `A query that was dehydrated as pending ended up rejecting. [${describeKey(query.queryKey)}]: ${error}; The error will be redacted in production builds`, ) } return Promise.reject(new Error('redacted')) diff --git a/packages/query-core/src/query.ts b/packages/query-core/src/query.ts index 62bc9a16082..5e9eb49c8fa 100644 --- a/packages/query-core/src/query.ts +++ b/packages/query-core/src/query.ts @@ -1,4 +1,5 @@ import { + describeKey, ensureQueryFn, noop, replaceData, @@ -569,10 +570,10 @@ export class Query< if (data === undefined) { if (process.env.NODE_ENV !== 'production') { console.error( - `Query data cannot be undefined. Please make sure to return a value other than undefined from your query function. Affected query key: ${this.queryHash}`, + `Query data cannot be undefined. Please make sure to return a value other than undefined from your query function. Affected query key: ${describeKey(this.queryKey)}`, ) } - throw new Error(`${this.queryHash} data is undefined`) + throw new Error(`${describeKey(this.queryKey)} data is undefined`) } this.setData(data) diff --git a/packages/query-core/src/utils.ts b/packages/query-core/src/utils.ts index f442ab86fdc..743e1c2b6b0 100644 --- a/packages/query-core/src/utils.ts +++ b/packages/query-core/src/utils.ts @@ -1,3 +1,4 @@ +import stableHash from 'stable-hash'; import { timeoutManager } from './timeoutManager' import type { DefaultError, @@ -230,16 +231,22 @@ export function hashQueryKeyByOptions( * Hashes the value into a stable hash. */ export function hashKey(queryKey: QueryKey | MutationKey): string { - return JSON.stringify(queryKey, (_, val) => - isPlainObject(val) - ? Object.keys(val) - .sort() - .reduce((result, key) => { - result[key] = val[key] - return result - }, {} as any) - : val, - ) + return stableHash(queryKey) +} + +/** + * Renders a key as a human readable string for logging and error messages. + * Keys are not guaranteed to be JSON serializable, so falls back to the hash. + */ +export function describeKey(key: QueryKey | MutationKey | undefined): string { + if (key === undefined) { + return 'undefined' + } + try { + return JSON.stringify(key) + } catch { + return hashKey(key) + } } /** @@ -411,7 +418,7 @@ export function replaceData< return replaceEqualDeep(prevData, data) } catch (error) { console.error( - `Structural sharing requires data to be JSON serializable. To fix this, turn off structuralSharing or return JSON-serializable data from your queryFn. [${options.queryHash}]: ${error}`, + `Structural sharing requires data to be JSON serializable. To fix this, turn off structuralSharing or return JSON-serializable data from your queryFn. [${describeKey(options.queryKey)}]: ${error}`, ) // Prevent the replaceEqualDeep from being called again down below. @@ -450,13 +457,14 @@ export function ensureQueryFn< options: { queryFn?: QueryFunction | SkipToken queryHash?: string + queryKey?: TQueryKey }, fetchOptions?: FetchOptions, ): QueryFunction { if (process.env.NODE_ENV !== 'production') { if (options.queryFn === skipToken) { console.error( - `Attempted to invoke queryFn when set to skipToken. This is likely a configuration error. Query hash: '${options.queryHash}'`, + `Attempted to invoke queryFn when set to skipToken. This is likely a configuration error. Query key: '${describeKey(options.queryKey)}'`, ) } } @@ -470,7 +478,7 @@ export function ensureQueryFn< if (!options.queryFn || options.queryFn === skipToken) { return () => - Promise.reject(new Error(`Missing queryFn: '${options.queryHash}'`)) + Promise.reject(new Error(`Missing queryFn: '${describeKey(options.queryKey)}'`)) } return options.queryFn diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 029ae0b6783..1357eabf4e9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2716,6 +2716,9 @@ importers: npm-run-all2: specifier: ^5.0.0 version: 5.0.2 + stable-hash: + specifier: ^0.0.6 + version: 0.0.6 packages/query-devtools: devDependencies: @@ -14738,6 +14741,9 @@ packages: resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} engines: {node: '>=12.0.0'} + stable-hash@0.0.6: + resolution: {integrity: sha512-0afH4mobqTybYZsXImQRLOjHV4gvOW+92HdUIax9t7a8d9v54KWykEuMVIcXhD9BCi+w3kS4x7O6fmZQ3JlG/g==} + stack-trace@1.0.0-pre2: resolution: {integrity: sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==} engines: {node: '>=16'} @@ -30719,6 +30725,8 @@ snapshots: stable-hash-x@0.2.0: {} + stable-hash@0.0.6: {} + stack-trace@1.0.0-pre2: {} stack-utils@2.0.6: From cb1fe5ea87d231874a11e979c7637050bd73a55c Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Sun, 19 Jul 2026 16:27:21 +0100 Subject: [PATCH 2/4] feat: handle legacy keys --- .../src/__tests__/hydration.test.tsx | 75 +++++++++++++++++-- packages/query-core/src/hydration.ts | 25 ++++--- packages/query-core/src/utils.ts | 6 +- 3 files changed, 84 insertions(+), 22 deletions(-) diff --git a/packages/query-core/src/__tests__/hydration.test.tsx b/packages/query-core/src/__tests__/hydration.test.tsx index 6fc60465c7b..2676690289d 100644 --- a/packages/query-core/src/__tests__/hydration.test.tsx +++ b/packages/query-core/src/__tests__/hydration.test.tsx @@ -5,7 +5,7 @@ import { QueryCache } from '../queryCache' import { dehydrate, hydrate } from '../hydration' import { MutationCache } from '../mutationCache' import { executeMutation, mockOnlineManagerIsOnline } from './utils' -import { hashKey } from '../utils'; +import { hashKey } from '../utils' describe('dehydration and rehydration', () => { beforeEach(() => { @@ -702,22 +702,18 @@ describe('dehydration and rehydration', () => { const dehydrated = dehydrate(queryClient) expect( - dehydrated.queries.find((q) => q.queryHash === metaKeyHash) - ?.meta, + dehydrated.queries.find((q) => q.queryHash === metaKeyHash)?.meta, ).toEqual({ some: 'meta', }) expect( - dehydrated.queries.find((q) => q.queryHash === noMetaKeyHash) - ?.meta, + dehydrated.queries.find((q) => q.queryHash === noMetaKeyHash)?.meta, ).toEqual(undefined) expect( Object.keys( - dehydrated.queries.find( - (q) => q.queryHash === noMetaKeyHash, - )!, + dehydrated.queries.find((q) => q.queryHash === noMetaKeyHash)!, ), ).not.toEqual(expect.arrayContaining(['meta'])) @@ -1911,4 +1907,67 @@ describe('dehydration and rehydration', () => { clientQueryClient.clear() serverQueryClient.clear() }) + + it('should hydrate under the recomputed hash when the payload carries a foreign queryHash', () => { + const key = queryKey() + const dehydrated = { + mutations: [], + queries: [ + { + queryKey: key, + queryHash: 'hash-from-another-implementation', + state: { + data: 'stale-hash data', + dataUpdatedAt: Date.now(), + status: 'success' as const, + fetchStatus: 'idle' as const, + }, + }, + ], + } + + const queryCache = new QueryCache() + const queryClient = new QueryClient({ queryCache }) + hydrate(queryClient, dehydrated) + + expect(queryCache.get('hash-from-another-implementation')).toBeUndefined() + expect(queryCache.get(hashKey(key))?.state.data).toBe('stale-hash data') + expect(queryClient.getQueryData(key)).toBe('stale-hash data') + expect(queryCache.getAll()).toHaveLength(1) + + queryClient.clear() + }) + + it('should hydrate using a custom queryKeyHashFn passed through hydrate options', () => { + const key = queryKey() + const queryKeyHashFn = (queryKey_: any) => `custom-${queryKey_[0]}` + const dehydrated = { + mutations: [], + queries: [ + { + queryKey: key, + queryHash: hashKey(key), + state: { + data: 'custom hash data', + dataUpdatedAt: Date.now(), + status: 'success' as const, + fetchStatus: 'idle' as const, + }, + }, + ], + } + + const queryCache = new QueryCache() + const queryClient = new QueryClient({ queryCache }) + hydrate(queryClient, dehydrated, { + defaultOptions: { queries: { queryKeyHashFn } }, + }) + + expect(queryCache.get(`custom-${key[0]}`)?.state.data).toBe( + 'custom hash data', + ) + expect(queryCache.getAll()).toHaveLength(1) + + queryClient.clear() + }) }) diff --git a/packages/query-core/src/hydration.ts b/packages/query-core/src/hydration.ts index fc0acd96204..74568c09a52 100644 --- a/packages/query-core/src/hydration.ts +++ b/packages/query-core/src/hydration.ts @@ -1,5 +1,5 @@ import { tryResolveSync } from './thenable' -import { describeKey, noop } from './utils' +import { describeKey, hashQueryKeyByOptions, noop } from './utils' import type { DefaultError, MutationKey, @@ -193,6 +193,11 @@ export function hydrate( client.getDefaultOptions().hydrate?.deserializeData ?? defaultTransformerFn + const queryDefaults = { + ...client.getDefaultOptions().hydrate?.queries, + ...options?.defaultOptions?.queries, + } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const mutations = (dehydratedState as DehydratedState).mutations || [] // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition @@ -211,15 +216,12 @@ export function hydrate( }) queries.forEach( - ({ - queryKey, - state, - queryHash, - meta, - promise, - dehydratedAt, - queryType, - }) => { + ({ queryKey, state, meta, promise, dehydratedAt, queryType }) => { + // The hash is recomputed rather than read from the payload, so + // that payloads written by the old hash implementation still resolve + // to the same cache entry + const queryHash = hashQueryKeyByOptions(queryKey, queryDefaults) + const syncData = promise ? tryResolveSync(promise) : undefined const rawData = state.data === undefined ? syncData?.data : state.data const data = rawData === undefined ? rawData : deserializeData(rawData) @@ -266,8 +268,7 @@ export function hydrate( query = queryCache.build( client, { - ...client.getDefaultOptions().hydrate?.queries, - ...options?.defaultOptions?.queries, + ...queryDefaults, queryKey, queryHash, meta, diff --git a/packages/query-core/src/utils.ts b/packages/query-core/src/utils.ts index 743e1c2b6b0..ce776b2f6c5 100644 --- a/packages/query-core/src/utils.ts +++ b/packages/query-core/src/utils.ts @@ -1,4 +1,4 @@ -import stableHash from 'stable-hash'; +import stableHash from 'stable-hash' import { timeoutManager } from './timeoutManager' import type { DefaultError, @@ -478,7 +478,9 @@ export function ensureQueryFn< if (!options.queryFn || options.queryFn === skipToken) { return () => - Promise.reject(new Error(`Missing queryFn: '${describeKey(options.queryKey)}'`)) + Promise.reject( + new Error(`Missing queryFn: '${describeKey(options.queryKey)}'`), + ) } return options.queryFn From 3e905a9b4ba1d9db8c97545670361ed92b4257ca Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Wed, 12 Aug 2026 16:49:07 +0100 Subject: [PATCH 3/4] chore: update devtools and tests to use user-friendly key stringify --- .../src/__tests__/useQuery.test.tsx | 4 +- .../src/__tests__/hydration.test.tsx | 66 +++++++++++++++++++ packages/query-core/src/hydration.ts | 5 +- packages/query-devtools/src/Devtools.tsx | 11 +++- .../src/__tests__/Devtools.test.tsx | 13 ++-- .../src/__tests__/retryStrategies.test.ts | 4 +- .../src/__tests__/useQuery.test.tsx | 4 +- 7 files changed, 91 insertions(+), 16 deletions(-) diff --git a/packages/preact-query/src/__tests__/useQuery.test.tsx b/packages/preact-query/src/__tests__/useQuery.test.tsx index 3a36725ff09..01fd845c970 100644 --- a/packages/preact-query/src/__tests__/useQuery.test.tsx +++ b/packages/preact-query/src/__tests__/useQuery.test.tsx @@ -6671,11 +6671,9 @@ describe('useQuery', () => { rendered.getByText('data: client') expect(count).toBe(1) - const query = clientQueryClient.getQueryCache().find({ queryKey: key }) - expect(consoleMock).toHaveBeenCalledTimes(1) expect(consoleMock).toHaveBeenCalledWith( - `A query that was dehydrated as pending ended up rejecting. [${query?.queryHash}]: Error: server error; The error will be redacted in production builds`, + `A query that was dehydrated as pending ended up rejecting. [${JSON.stringify(key)}]: Error: server error; The error will be redacted in production builds`, ) consoleMock.mockRestore() diff --git a/packages/query-core/src/__tests__/hydration.test.tsx b/packages/query-core/src/__tests__/hydration.test.tsx index 2676690289d..650f29b0e97 100644 --- a/packages/query-core/src/__tests__/hydration.test.tsx +++ b/packages/query-core/src/__tests__/hydration.test.tsx @@ -1938,6 +1938,72 @@ describe('dehydration and rehydration', () => { queryClient.clear() }) + it('should hydrate using a custom queryKeyHashFn from the client defaults', () => { + const key = queryKey() + const queryKeyHashFn = (queryKey_: any) => `client-default-${queryKey_[0]}` + const dehydrated = { + mutations: [], + queries: [ + { + queryKey: key, + queryHash: hashKey(key), + state: { + data: 'client default data', + dataUpdatedAt: Date.now(), + status: 'success' as const, + fetchStatus: 'idle' as const, + }, + }, + ], + } + + const queryCache = new QueryCache() + const queryClient = new QueryClient({ + queryCache, + defaultOptions: { queries: { queryKeyHashFn } }, + }) + hydrate(queryClient, dehydrated) + + expect(queryCache.get(`client-default-${key[0]}`)?.state.data).toBe( + 'client default data', + ) + expect(queryClient.getQueryData(key)).toBe('client default data') + expect(queryCache.getAll()).toHaveLength(1) + + queryClient.clear() + }) + + it('should hydrate using a custom queryKeyHashFn from setQueryDefaults', () => { + const key = queryKey() + const queryKeyHashFn = (queryKey_: any) => `per-key-${queryKey_[0]}` + const dehydrated = { + mutations: [], + queries: [ + { + queryKey: key, + queryHash: hashKey(key), + state: { + data: 'per-key data', + dataUpdatedAt: Date.now(), + status: 'success' as const, + fetchStatus: 'idle' as const, + }, + }, + ], + } + + const queryCache = new QueryCache() + const queryClient = new QueryClient({ queryCache }) + queryClient.setQueryDefaults(key, { queryKeyHashFn }) + hydrate(queryClient, dehydrated) + + expect(queryCache.get(`per-key-${key[0]}`)?.state.data).toBe('per-key data') + expect(queryClient.getQueryData(key)).toBe('per-key data') + expect(queryCache.getAll()).toHaveLength(1) + + queryClient.clear() + }) + it('should hydrate using a custom queryKeyHashFn passed through hydrate options', () => { const key = queryKey() const queryKeyHashFn = (queryKey_: any) => `custom-${queryKey_[0]}` diff --git a/packages/query-core/src/hydration.ts b/packages/query-core/src/hydration.ts index 74568c09a52..a55572405ad 100644 --- a/packages/query-core/src/hydration.ts +++ b/packages/query-core/src/hydration.ts @@ -220,7 +220,10 @@ export function hydrate( // The hash is recomputed rather than read from the payload, so // that payloads written by the old hash implementation still resolve // to the same cache entry - const queryHash = hashQueryKeyByOptions(queryKey, queryDefaults) + const queryHash = hashQueryKeyByOptions( + queryKey, + client.defaultQueryOptions({ ...queryDefaults, queryKey }), + ) const syncData = promise ? tryResolveSync(promise) : undefined const rawData = state.data === undefined ? syncData?.data : state.data diff --git a/packages/query-devtools/src/Devtools.tsx b/packages/query-devtools/src/Devtools.tsx index daff934932f..6c65344ef4f 100644 --- a/packages/query-devtools/src/Devtools.tsx +++ b/packages/query-devtools/src/Devtools.tsx @@ -737,7 +737,10 @@ export const ContentView: Component = (props) => { let filtered = props.localStore.filter ? curr.filter( (item) => - rankItem(item.queryHash, props.localStore.filter || '').passed, + rankItem( + displayValue(item.queryKey), + props.localStore.filter || '', + ).passed, ) : [...curr] @@ -1457,14 +1460,16 @@ const QueryRow: Component<{ query: Query }> = (props) => { styles().selectedQueryRow, 'tsqd-query-row', )} - aria-label={`Query key ${props.query.queryHash}${isDisabled() ? ', disabled' : ''}${isStatic() ? ', static' : ''}`} + aria-label={`Query key ${displayValue(props.query.queryKey)}${isDisabled() ? ', disabled' : ''}${isStatic() ? ', static' : ''}`} >
{observers()}
- {props.query.queryHash} + + {displayValue(props.query.queryKey)} +