Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
29eb336
fix(react-query): resolve hydration mismatch in SSR with prefetched q…
joseph0926 Aug 17, 2025
ab2ddad
fix(react-query): use QueryObserverPendingResult type and clarify fie…
joseph0926 Aug 17, 2025
907def0
fix(react-query): refactor clarify field selection
joseph0926 Aug 17, 2025
184be02
fix(react-query): ensure complete pending state invariants in getServ…
joseph0926 Aug 17, 2025
5f14a66
fix(react-query): ensure complete pending state invariants in getServ…
joseph0926 Aug 17, 2025
cb50d25
fix(react-query): add isRefetching field
joseph0926 Aug 17, 2025
25fc95e
fix(react-query): use query state for fetchStatus in SSR hydration ma…
joseph0926 Aug 19, 2025
3429d75
Merge branch 'main' into fix/ssr-hydration-mismatch
joseph0926 Aug 20, 2025
c65c2f9
Merge branch 'main' into fix/ssr-hydration-mismatch
joseph0926 Aug 30, 2025
e69a205
Merge branch 'main' into fix/ssr-hydration-mismatch
joseph0926 Aug 31, 2025
7996780
Merge branch 'main' into fix/ssr-hydration-mismatch
joseph0926 Sep 1, 2025
af8b955
refactor(query-core): use explicit field definitions in getServerResu…
joseph0926 Sep 2, 2025
1bf795b
fix(react-query): add getServerResult support to useQueries hook
joseph0926 Sep 2, 2025
d2feecb
test(react-query): add a test for combine() behavior under SSR snapshots
joseph0926 Sep 2, 2025
261326b
test(react-query): improving tests for combine() behavior in SSR snap…
joseph0926 Sep 2, 2025
a0859fa
test(react-query): clarify test title
joseph0926 Sep 2, 2025
58065ac
Merge branch 'main' into fix/ssr-hydration-mismatch
joseph0926 Sep 3, 2025
d43b020
Merge branch 'main' into fix/ssr-hydration-mismatch
joseph0926 Sep 4, 2025
a8feb9e
Merge branch 'main' into fix/ssr-hydration-mismatch
joseph0926 Sep 4, 2025
e19ca01
Merge branch 'main' into fix/ssr-hydration-mismatch
joseph0926 Sep 6, 2025
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
180 changes: 180 additions & 0 deletions packages/query-core/src/__tests__/queriesObserver.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -347,4 +347,184 @@ describe('queriesObserver', () => {
expect(queryFn1).toHaveBeenCalledTimes(1)
expect(queryFn2).toHaveBeenCalledTimes(1)
})

describe('SSR Hydration', () => {
describe('Hydration Mismatch Problem', () => {
test('should demonstrate hydration mismatch issue with multiple queries (before fix)', () => {
const key1 = queryKey()
const key2 = queryKey()

queryClient.setQueryData(key1, { amount: 10 })
queryClient.setQueryData(key2, { amount: 20 })

const cache1 = queryClient.getQueryCache().find({ queryKey: key1 })
const cache2 = queryClient.getQueryCache().find({ queryKey: key2 })

if (cache1) {
cache1.state.dataUpdatedAt = 0
cache1.state.fetchStatus = 'idle'
}
if (cache2) {
cache2.state.dataUpdatedAt = 0
cache2.state.fetchStatus = 'idle'
}

const observer = new QueriesObserver(queryClient, [
{ queryKey: key1, queryFn: () => ({ amount: 10 }) },
{ queryKey: key2, queryFn: () => ({ amount: 20 }) },
])

const clientResults = observer.getCurrentResult()

expect(clientResults[0]).toMatchObject({
status: 'success',
data: { amount: 10 },
isLoading: false,
isPending: false,
})
expect(clientResults[1]).toMatchObject({
status: 'success',
data: { amount: 20 },
isLoading: false,
isPending: false,
})
})
})

describe('Solution with getServerResult', () => {
test('getServerResult should return pending state for hydrated queries', () => {
const key1 = queryKey()
const key2 = queryKey()

queryClient.setQueryData(key1, { amount: 10 })
queryClient.setQueryData(key2, { amount: 20 })

const cache1 = queryClient.getQueryCache().find({ queryKey: key1 })
const cache2 = queryClient.getQueryCache().find({ queryKey: key2 })

if (cache1) {
cache1.state.dataUpdatedAt = 0
cache1.state.fetchStatus = 'idle'
}
if (cache2) {
cache2.state.dataUpdatedAt = 0
cache2.state.fetchStatus = 'idle'
}

const observer = new QueriesObserver(queryClient, [
{ queryKey: key1, queryFn: () => ({ amount: 10 }) },
{ queryKey: key2, queryFn: () => ({ amount: 20 }) },
])

const clientResults = observer.getCurrentResult()
const serverResults = observer.getServerResult()

expect(clientResults[0]).toMatchObject({
status: 'success',
data: { amount: 10 },
isLoading: false,
})
expect(serverResults[0]).toMatchObject({
status: 'pending',
data: undefined,
isLoading: false,
isPending: true,
isSuccess: false,
})

expect(clientResults[1]).toMatchObject({
status: 'success',
data: { amount: 20 },
isLoading: false,
})
expect(serverResults[1]).toMatchObject({
status: 'pending',
data: undefined,
isLoading: false,
isPending: true,
isSuccess: false,
})
})

test('should handle mixed hydrated and non-hydrated queries', () => {
const key1 = queryKey()
const key2 = queryKey()

queryClient.setQueryData(key1, { amount: 10 })
queryClient.setQueryData(key2, { amount: 20 })

const cache1 = queryClient.getQueryCache().find({ queryKey: key1 })
const cache2 = queryClient.getQueryCache().find({ queryKey: key2 })

if (cache1) {
cache1.state.dataUpdatedAt = 0
cache1.state.fetchStatus = 'idle'
}
if (cache2) {
cache2.state.dataUpdatedAt = Date.now()
cache2.state.fetchStatus = 'idle'
}

const observer = new QueriesObserver(queryClient, [
{ queryKey: key1, queryFn: () => ({ amount: 10 }) },
{ queryKey: key2, queryFn: () => ({ amount: 20 }) },
])

const serverResults = observer.getServerResult()

expect(serverResults[0]).toMatchObject({
status: 'pending',
data: undefined,
isPending: true,
})

expect(serverResults[1]).toMatchObject({
status: 'success',
data: { amount: 20 },
isPending: false,
})
})

test('should handle fetching state during hydration for multiple queries', () => {
const key1 = queryKey()
const key2 = queryKey()

queryClient.setQueryData(key1, { amount: 10 })
queryClient.setQueryData(key2, { amount: 20 })

const cache1 = queryClient.getQueryCache().find({ queryKey: key1 })
const cache2 = queryClient.getQueryCache().find({ queryKey: key2 })

if (cache1) {
cache1.state.dataUpdatedAt = 0
cache1.state.fetchStatus = 'fetching'
}
if (cache2) {
cache2.state.dataUpdatedAt = 0
cache2.state.fetchStatus = 'idle'
}

const observer = new QueriesObserver(queryClient, [
{ queryKey: key1, queryFn: () => ({ amount: 10 }) },
{ queryKey: key2, queryFn: () => ({ amount: 20 }) },
])

const serverResults = observer.getServerResult()

expect(serverResults[0]).toMatchObject({
status: 'pending',
fetchStatus: 'fetching',
isLoading: true,
isPending: true,
})

expect(serverResults[1]).toMatchObject({
status: 'pending',
fetchStatus: 'idle',
isLoading: false,
isPending: true,
})
})
})
})
})
Loading
Loading