Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/react/basic/src/index.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,7 @@ function Posts({
}: {
setPostId: React.Dispatch<React.SetStateAction<number>>
}) {
const queryClient = useQueryClient()
const postQueryClient = useQueryClient()
const { status, data, error, isFetching } = usePosts()

return (
Expand All@@ -60,7 +60,7 @@ function Posts({
style={
// We can access the query data here to show bold links for
// ones that are cached
queryClient.getQueryData(['post', post.id])
postQueryClient.getQueryData(['post', post.id])
? {
fontWeight: 'bold',
color: 'green',
Expand Down
4 changes: 1 addition & 3 deletions packages/preact-query/src/__tests__/useQuery.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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()
Expand Down
3 changes: 2 additions & 1 deletion packages/query-core/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -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"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
148 changes: 139 additions & 9 deletions packages/query-core/src/__tests__/hydration.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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(() => {
Expand DownExpand Up@@ -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 })

Expand DownExpand Up@@ -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)
Expand All@@ -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({
Expand All@@ -698,22 +702,18 @@ describe('dehydration and rehydration', () => {
const dehydrated = dehydrate(queryClient)

expect(
dehydrated.queries.find((q) => q.queryHash === JSON.stringify(metaKey))
?.meta,
dehydrated.queries.find((q) => q.queryHash === metaKeyHash)?.meta,
).toEqual({
some: 'meta',
})

expect(
dehydrated.queries.find((q) => q.queryHash === JSON.stringify(noMetaKey))
?.meta,
dehydrated.queries.find((q) => q.queryHash === noMetaKeyHash)?.meta,
).toEqual(undefined)

expect(
Object.keys(
dehydrated.queries.find(
(q) => q.queryHash === JSON.stringify(noMetaKey),
)!,
dehydrated.queries.find((q) => q.queryHash === noMetaKeyHash)!,
),
).not.toEqual(expect.arrayContaining(['meta']))

Expand DownExpand Up@@ -798,6 +798,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 = {
Expand All@@ -811,7 +812,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)
Expand DownExpand Up@@ -1906,4 +1907,133 @@ 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 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]}`
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()
})
})
Original file line numberDiff line numberDiff line change
Expand Up@@ -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()
})

Expand DownExpand Up@@ -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()
Expand Down
3 changes: 1 addition & 2 deletions packages/query-core/src/__tests__/query.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -885,10 +885,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)}'`),
Comment thread
43081j marked this conversation as resolved.
})
unsubscribe()
})
Expand Down
12 changes: 6 additions & 6 deletions packages/query-core/src/__tests__/utils.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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', () => {
Expand DownExpand Up@@ -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', () => {
Expand All@@ -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', () => {
Expand DownExpand Up@@ -641,7 +641,7 @@ describe('core/utils', () => {

const resolved = ensureQueryFn({
queryFn: skipToken,
queryHash: '["skip"]',
queryKey: ['skip'] as QueryKey,
})

expect(consoleErrorSpy).toHaveBeenCalledWith(
Expand Down
30 changes: 17 additions & 13 deletions packages/query-core/src/hydration.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { tryResolveSync } from './thenable'
import { noop } from './utils'
import { describeKey, hashQueryKeyByOptions, noop } from './utils'
import type {
DefaultError,
MutationKey,
Expand DownExpand Up@@ -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'))
Expand DownExpand Up@@ -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
Expand All@@ -211,15 +216,15 @@ 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,
client.defaultQueryOptions({ ...queryDefaults, queryKey }),
)

Comment thread
coderabbitai[bot] marked this conversation as resolved.
const syncData = promise ? tryResolveSync(promise) : undefined
const rawData = state.data === undefined ? syncData?.data : state.data
const data = rawData === undefined ? rawData : deserializeData(rawData)
Expand DownExpand Up@@ -266,8 +271,7 @@ export function hydrate(
query = queryCache.build(
client,
{
...client.getDefaultOptions().hydrate?.queries,
...options?.defaultOptions?.queries,
...queryDefaults,
queryKey,
queryHash,
meta,
Expand Down
Loading