Repository navigation
fix(react-query): resolve hydration mismatch in SSR with prefetched queries #9572
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
29eb336
ab2ddad
907def0
184be02
5f14a66
cb50d25
25fc95e
3429d75
c65c2f9
e69a205
7996780
af8b955
1bf795b
d2feecb
261326b
a0859fa
58065ac
d43b020
a8feb9e
e19ca01
253cb40
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,7 @@ import type { | |
| QueryKey, | ||
| QueryObserverBaseResult, | ||
| QueryObserverOptions, | ||
| QueryObserverPendingResult, | ||
| QueryObserverResult, | ||
| QueryOptions, | ||
| RefetchOptions, | ||
|
|
@@ -264,6 +265,31 @@ export class QueryObserver< | |
| return this.#currentResult | ||
| } | ||
|
|
||
| getServerResult(): | ||
| | QueryObserverPendingResult<TData, TError> | ||
| | QueryObserverResult<TData, TError> { | ||
| const currentResult = this.#currentResult | ||
|
|
||
| const isHydratedData = | ||
| currentResult.status === 'success' && | ||
| this.#currentQuery.state.dataUpdatedAt === 0 | ||
|
|
||
| if (isHydratedData) { | ||
| return { | ||
| ...currentResult, | ||
| status: 'pending' as const, | ||
| isPending: true, | ||
| isSuccess: false, | ||
| data: undefined, | ||
| isLoading: currentResult.fetchStatus === 'fetching', | ||
| isInitialLoading: currentResult.fetchStatus === 'fetching', | ||
| isPlaceholderData: false, | ||
|
Comment on lines
+276
to
+296
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how did you come up with this list of fields to change? For example, why aren’t fields like I’m wondering how we can make sure to keep this list in-sync if we add new properties to
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for your feedback! In conclusion, the criteria for selecting the specified fields was “only change fields that directly affect SSR hydration inconsistencies.” I believe that intentionally maintaining the currentResult value for the remaining fields will be beneficial during subsequent maintenance processes. However, based on coderabbitai's feedback, I realized that I had overlooked |
||
| } | ||
|
Comment on lines
+274
to
+301
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainVerify tests assert the key pending invariants Please confirm tests cover isRefetching === false, isFetched === false, and isFetchedAfterMount === false for hydrated snapshots (and optionally that failureCount/reset fields are as intended). Run: 🏁 Script executed: #!/bin/bash
# Assert server-snapshot tests exist
rg -n -C2 "getServerResult|server snapshot|SSR|hydration" packages/query-core/src/__tests__ -g '*.ts' -g '*.tsx'
# Check for invariants on hydrated server results
rg -n -C2 "isRefetching\W*toBe\((false|0)\)|isFetched\W*toBe\((false|0)\)|isFetchedAfterMount\W*toBe\((false|0)\)" \
packages/query-core/src/__tests__ -g '*.ts' -g '*.tsx'Length of output: 46044 Add missing invariant assertions in SSR hydration tests
🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| return currentResult | ||
| } | ||
|
Comment on lines
+268
to
+307
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Fix inconsistent flags for pending server snapshot (isRefetching, error flags, fetched flags). When transforming a hydrated success result into a pending server snapshot, we spread
Recommend explicitly overriding these to match Apply this diff inside if (isHydratedData) {
- return {
- ...currentResult,
- status: 'pending' as const,
- isPending: true,
- isSuccess: false,
- data: undefined,
- isLoading: currentResult.fetchStatus === 'fetching',
- isInitialLoading: currentResult.fetchStatus === 'fetching',
- isPlaceholderData: false,
- }
+ return {
+ ...currentResult,
+ status: 'pending' as const,
+ isPending: true,
+ isSuccess: false,
+ // pending invariants
+ isError: false,
+ error: null,
+ isLoadingError: false,
+ isRefetchError: false,
+ // data is not available for pending
+ data: undefined,
+ // compute loading flags from fetchStatus
+ isLoading: currentResult.fetchStatus === 'fetching',
+ isInitialLoading: currentResult.fetchStatus === 'fetching',
+ // pending cannot be "refetching"
+ isRefetching: false,
+ // do not report placeholder data
+ isPlaceholderData: false,
+ // avoid leaking fetched flags from the success snapshot
+ isFetched: false,
+ isFetchedAfterMount: false,
+ } as QueryObserverPendingResult<TData, TError>
}🤖 Prompt for AI Agents💡 Verification agent 🧩 Analysis chainAdd test coverage for isRefetching and fetched flags in server snapshot. To prevent regressions, assert in tests that:
[offer_assistance] If missing, I can provide a focused test case to assert these invariants. 🏁 Script executed: #!/bin/bash
# Locate existing tests around getServerResult and hydration
rg -n -C3 "getServerResult|server snapshot|hydration" --type=ts --type=tsx
# Check if isRefetching/isFetched assertions exist
rg -n -C2 "isRefetching|isFetchedAfterMount|isFetched" packages/query-core/src/__tests__/queryObserver.test.tsxLength of output: 218 🏁 Script executed: #!/bin/bash
# Retry pattern search without relying on --type (tsx not recognized)
rg -n -C3 "getServerResult|server snapshot|hydration" -g '*.ts' -g '*.tsx'
rg -n -C2 "isRefetching|isFetchedAfterMount|isFetched" -g '*.ts' -g '*.tsx'Length of output: 120039 Add missing assertions for The existing hydration tests in • After the “should return pending state for hydrated data” test (around line 1530), add: expect(serverResult.isFetched).toBe(false)
expect(serverResult.isFetchedAfterMount).toBe(false)
expect(serverResult.isRefetching).toBe(false)• After the “should handle fetching state during hydration” test (around line 1575), also assert: expect(serverResult.isFetched).toBe(false)
expect(serverResult.isFetchedAfterMount).toBe(false)
expect(serverResult.isRefetching).toBe(false)This will ensure that hydrated snapshots never report fetched or refetching states on the server. 🤖 Prompt for AI Agents |
||
|
|
||
| trackResult( | ||
| result: QueryObserverResult<TData, TError>, | ||
| onPropTracked?: (key: keyof QueryObserverResult) => void, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Use a non-zero constant (or set system time) to avoid flakiness with fake timers
Under fake timers, Date.now() may be 0 unless you set the system time. That would unintentionally mark this as “hydrated” (dataUpdatedAt === 0), causing
getServerResultto mask the result and fail the “non-hydrated” expectations.Minimal, deterministic fix:
Alternatively, set the mocked clock first:
🏁 Script executed:
Length of output: 3741
Use a non-zero constant for
dataUpdatedAtto avoid flakiness under fake timersUnder
vi.useFakeTimers(),Date.now()returns0unless you explicitly advance the clock—making your “fresh” timestamp indistinguishable from the unhydrated state. Update the assignment inpackages/query-core/src/__tests__/queryObserver.test.tsxto use a non-zero literal (or advance the fake clock first):• Minimal, consistent with other direct state mutations:
• Alternative (advance the fake clock, then read):
🤖 Prompt for AI Agents