Skip to content

fix(useQuery): prevent hydration mismatch when ssr: false and skip: true are combined#13128

Merged
phryneas merged 15 commits intoapollographql:mainfrom
pavelivanov:fix/prevent-hydration-mismatch
Feb 23, 2026
Merged

fix(useQuery): prevent hydration mismatch when ssr: false and skip: true are combined#13128
phryneas merged 15 commits intoapollographql:mainfrom
pavelivanov:fix/prevent-hydration-mismatch

Conversation

@pavelivanov
Copy link
Copy Markdown
Contributor

@pavelivanov pavelivanov commented Feb 4, 2026

Summary

  • Fixes hydration mismatch error when using useQuery with both skip: true and ssr: false options
  • Aligns getServerSnapshot behavior with useSSRQuery by checking skip before ssr

Problem

When using useQuery with both skip: true and ssr: false, a hydration mismatch occurs because:

  1. Server-side (useSSRQuery): Checks skip first → returns loading: false
  2. Client hydration (getServerSnapshot): Only checks ssr: false → returns loading: true

This causes React to throw a hydration error since server rendered loading: false but client initially shows loading: true.

Code flow before fix:

Phase Code Path Result
Server SSR useSSRQuery checks skip FIRST loading: false
Client Hydration getServerSnapshot checks ssr === false only loading: true
After Hydration getSnapshot returns observable state loading: false

Solution

Modified getServerSnapshot in useQuery.ts to check isSkipped before checking ssr === false, matching the condition ordering in useSSRQuery:

// Before (line 594)
() => (ssr === false ? useQuery.ssrDisabledResult : resultData.current)

// After
() =>
  isSkipped ? resultData.current
  : ssr === false ? useQuery.ssrDisabledResult
  : resultData.current

Test plan

- Added test: "should return loading: false when both ssr: false and skip: true are set"
- Added test: "should return loading: false when skipToken is used with ssr: false in defaultOptions"
- All existing SSR tests pass (35 tests)
- All existing skip-related tests pass (96 tests)

Checklist

- Tests added for the bug fix
- No breaking changes
- Matches existing useSSRQuery behavior

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **Bug Fixes**
* Resolve hydration mismatch when queries are skipped during SSR (also accounts for no-cache fetch policy), ensuring consistent server/client loading state.

* **Tests**
* Add end-to-end test validating SSR/hydration behavior for skipped, client-only queries.

* **Chores**
* Add a changelog entry for the patch release and update test config to exclude the new end-to-end test in standard React 18 runs.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

@apollo-cla
Copy link
Copy Markdown

@pavelivanov: Thank you for submitting a pull request! Before we can merge it, you'll need to sign the Apollo Contributor License Agreement here: https://contribute.apollographql.com/

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Feb 4, 2026

🦋 Changeset detected

Latest commit: ca88f33

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pavelivanov
Copy link
Copy Markdown
Contributor Author

pavelivanov commented Feb 17, 2026

@phryneas hi, please can you help me understand how to assign a reviewer? I don't have such action in the UI.

Copy link
Copy Markdown
Member

@phryneas phryneas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pavelivanov
Thank you for the PR, sorry for the late response - I kinda missed this one.
I've taken a look, and I agree with the problem, but I think we should try to fix as much as possible from this over in useSSRQuery, as any addition to useQuery will make it into the client bundle.

That means that the two blocks

  if (
    options === skipToken ||
    options.skip ||
    options.fetchPolicy === "standby"
  ) {
    return withoutObservableAccess({
      ...baseResult,
      ...skipStandbyResult,
    });
  }

and

  if (options.ssr === false || options.fetchPolicy === "no-cache") {
    return withoutObservableAccess({
      ...baseResult,
      ...useQuery.ssrDisabledResult,
    });
  }

in there would need to switch places.

The only change in useQuery in the end should be

-    () => (ssr === false ? useQuery.ssrDisabledResult : resultData.current)
+    () => (ssr === false || observable.options.fetchPolicy === "no-cache" ? useQuery.ssrDisabledResult : resultData.current)

Could you please implement these changes?

Comment on lines +6 to +7

When both options were combined, the server would return `loading: false` (because `useSSRQuery` checks `skip` first), but the client's `getServerSnapshot` was returning `ssrDisabledResult` with `loading: true`, causing a hydration mismatch. This fix updates `getServerSnapshot` to check `isSkipped` before the `ssr` option to match server behavior.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
When both options were combined, the server would return `loading: false` (because `useSSRQuery` checks `skip` first), but the client's `getServerSnapshot` was returning `ssrDisabledResult` with `loading: true`, causing a hydration mismatch. This fix updates `getServerSnapshot` to check `isSkipped` before the `ssr` option to match server behavior.
When both options were combined, the server would return `loading: false` (because `useSSRQuery` checks `skip` first), but the client's `getServerSnapshot` was returning `ssrDisabledResult` with `loading: true`, causing a hydration mismatch.

- prioritize ssr: false over skip in useSSRQuery so the server always returns ssrDisabledResult (loading: true), matching the client’s getServerSnapshot and preventing hydration mismatches.
- handle fetchPolicy: "no-cache" in getServerSnapshot to avoid similar inconsistencies.
@pavelivanov
Copy link
Copy Markdown
Contributor Author

@phryneas
Thank you for the suggestions. I've made the updates, please take a look 938981d.

@pavelivanov
Copy link
Copy Markdown
Contributor Author

I leave this for history:

SSR test (ssr/\_\_tests\_\_/useQuery.test.tsx) - tests the server render via renderToStringWithData, which uses useSSRQuery under the hood. With ssr: false + skip: true, the ssr: false check runs first and returns ssrDisabledResultloading: true. This is what gets baked into the server HTML.

Hooks test (hooks/\_\_tests\_\_/useQuery.test.tsx) - tests the client render via renderHookToSnapshotStream, which is a normal client-side render (no SSR/hydration
involved). This calls the regular getSnapshot callback (() => resultData.current), and since the query is skipped, the observable produces loading: false.

Why no hydration mismatch:

useSyncExternalStore takes 3 callbacks:

  1. subscribe - sets up the subscription
  2. getSnapshot - used on the client (() => resultData.current)
  3. getServerSnapshot - used only during hydration to match server HTML

During hydration, React calls getServerSnapshot (not getSnapshot). Our getServerSnapshot is:

  () =>
    ssr === false || observable.options.fetchPolicy === "no-cache"
      ? useQuery.ssrDisabledResult  // loading: true
      : resultData.current

So during hydration with ssr: false, it returns loading: true - matching the server HTML. No mismatch.

After hydration completes, React switches to getSnapshot which returns resultData.current (loading: false). This is a normal post-hydration state update, not a hydration error.

The hydration test (https://github.com/apollographql/apollo-client/pull/13128/changes#diff-d2c5924a921e0cb39c6d9910863c40eaab64a7901aa9414ab702a6eda79f7e82R1204) validates this exact flow end-to-end: renderToStringWithDatahydrateRoot → asserts zero hydration errors.

@pavelivanov pavelivanov requested a review from phryneas February 18, 2026 16:34
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 19, 2026

Warning

Rate limit exceeded

@phryneas has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 5 minutes and 31 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 355900a and ca88f33.

📒 Files selected for processing (1)
  • config/jest.config.ts
📝 Walkthrough

Walkthrough

Fixes a hydration mismatch in useQuery when ssr: false and skip: true by changing SSR data provisioning to consider fetchPolicy, adds an end-to-end hydration test, updates Jest ignores, and adds a changeset documenting the patch.

Changes

Cohort / File(s) Summary
Changelog
\.changeset/fix-usequery-ssr-skip-hydration.md
Adds a changeset documenting a patch release that fixes hydration mismatch for useQuery when ssr: false and skip: true.
Hook Logic
src/react/hooks/useQuery.ts
Adjusts SSR return logic to choose the SSR-disabled placeholder when either (a) fetchPolicy is not "standby" and ssr is false, or (b) fetchPolicy is "no-cache"; otherwise returns existing resultData.current.
E2E Test
src/react/ssr/__tests__/useQueryEndToEnd.test.tsx
Adds an end-to-end test that prerenders with a MockLink and then hydrates, asserting server-rendered state, no hydration errors, post-hydration state transitions, and that server cache remains empty after prerender.
Test Config
config/jest.config.ts
Adds src/react/ssr/__tests__/useQueryEndToEnd.test.tsx to the React 18 Jest testPathIgnorePatterns list to avoid running this test under standard React 18 config.

Sequence Diagram(s)

mermaid
sequenceDiagram
actor ServerRenderer
participant ApolloClient_SSR
participant HTML
participant ClientHydrator
participant ApolloClient_Client
participant BrowserDOM

ServerRenderer->>ApolloClient_SSR: render component (useQuery ssr:false, skip:true)
ApolloClient_SSR-->>ServerRenderer: produce SSR render state (loading/data/networkStatus)
ServerRenderer->>HTML: emit server markup
HTML->>ClientHydrator: deliver markup for hydration
ClientHydrator->>ApolloClient_Client: getServerSnapshot / initialize client state
ApolloClient_Client-->>ClientHydrator: ssr-disabled snapshot (matching server state after fix)
ClientHydrator->>BrowserDOM: hydrate and update UI

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main fix: preventing hydration mismatch when ssr: false and skip: true are combined, which directly matches the core problem and solution in the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fd42142 and 6f296b0.

📒 Files selected for processing (5)
  • .changeset/fix-usequery-ssr-skip-hydration.md
  • src/react/hooks/__tests__/useQuery.test.tsx
  • src/react/hooks/useQuery.ts
  • src/react/ssr/__tests__/useQuery.test.tsx
  • src/react/ssr/useSSRQuery.ts
🧰 Additional context used
🧬 Code graph analysis (4)
src/react/hooks/useQuery.ts (2)
src/react/index.ts (1)
  • useQuery (8-8)
src/react/index.react-server.ts (1)
  • useQuery (42-42)
src/react/hooks/__tests__/useQuery.test.tsx (6)
src/core/ApolloClient.ts (2)
  • query (982-1036)
  • ApolloClient (700-1685)
src/core/ObservableQuery.ts (1)
  • query (312-314)
src/core/index.ts (3)
  • gql (183-183)
  • NetworkStatus (30-30)
  • ApolloClient (17-17)
src/react/hooks/useQuery.ts (1)
  • useQuery (380-399)
src/testing/react/MockedProvider.tsx (1)
  • MockedProvider (33-84)
src/testing/core/mocking/mockLink.ts (1)
  • MockLink (90-261)
src/react/ssr/useSSRQuery.ts (2)
src/react/hooks/useQuery.ts (1)
  • useQuery (380-399)
src/react/index.ts (2)
  • useQuery (8-8)
  • skipToken (18-18)
src/react/ssr/__tests__/useQuery.test.tsx (3)
src/react/hooks/useQuery.ts (1)
  • useQuery (380-399)
src/react/index.ts (1)
  • useQuery (8-8)
src/react/index.react-server.ts (1)
  • useQuery (42-42)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build and Test
🔇 Additional comments (7)
.changeset/fix-usequery-ssr-skip-hydration.md (1)

1-7: LGTM!

The changeset correctly documents the hydration mismatch fix as a patch release. The description accurately captures the root cause (ordering mismatch between useSSRQuery and getServerSnapshot) and the behavioral change.

src/react/ssr/__tests__/useQuery.test.tsx (1)

131-156: LGTM!

The test correctly validates the fixed SSR behavior:

  • Updated description accurately reflects the new expectation (loading: true during SSR).
  • The inline comment on lines 137-139 clearly explains the priority semantics (ssr: false over skip: true) and how this prevents hydration mismatches.
  • Assertions align with the ssrDisabledResult behavior described in the PR objectives.
src/react/hooks/__tests__/useQuery.test.tsx (2)

1123-1158: Covers the ssr:false + skip:true client result as intended.
Nice focused regression check for the loading state.


1203-1272: Hydration mismatch regression test looks solid.
Good coverage of server render → hydrateRoot → no recoverable errors.

src/react/hooks/useQuery.ts (1)

594-598: LGTM: getServerSnapshot treats no-cache as SSR-disabled.

Line 595 now mirrors the SSR gating logic by returning ssrDisabledResult for fetchPolicy: "no-cache", which should prevent hydration mismatches.

src/react/ssr/useSSRQuery.ts (2)

49-56: LGTM: SSR-disabled branch now covers ssr: false and no-cache.

Line 50-56 ensures SSR returns the disabled placeholder for both cases, aligning SSR output with client hydration logic.


59-67: LGTM: skip/standby branch preserved after SSR-disabled check.

Line 60-67 keeps the skip/standby result intact once SSR-disabled scenarios are ruled out, maintaining consistent behavior.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/react/hooks/__tests__/useQuery.test.tsx`:
- Around line 1160-1201: The test name claims it uses "ssr: false in
defaultOptions" but the ApolloClient is created without defaultOptions; update
the ApolloClient instantiation in this test (the new ApolloClient({...}) call)
to include defaultOptions: { watchQuery: { ssr: false } } so the test actually
exercises that behavior, or alternatively rename the it(...) description to
reflect that only ssrMode: false is set (e.g., "should return loading: false on
the client when skipToken is used with ssrMode: false"); pick one approach and
make the corresponding change to keep the test name accurate.

Comment on lines +1160 to +1201
it("should return loading: false on the client when skipToken is used with ssr: false in defaultOptions", async () => {
const query = gql`
{
hello
}
`;
const mocks = [
{
request: { query },
result: { data: { hello: "world" } },
},
];

const client = new ApolloClient({
cache: new InMemoryCache(),
link: new MockLink(mocks),
ssrMode: false,
});

using _disabledAct = disableActEnvironment();
const { takeSnapshot } = await renderHookToSnapshotStream(
() => useQuery(query, skipToken),
{
wrapper: ({ children }) => (
<ApolloProvider client={client}>{children}</ApolloProvider>
),
}
);

{
const result = await takeSnapshot();

expect(result).toStrictEqualTyped({
data: undefined,
dataState: "empty",
loading: false,
networkStatus: NetworkStatus.ready,
previousData: undefined,
variables: {},
});
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Test name doesn’t match the setup (no defaultOptions configured).
The client here doesn’t set defaultOptions, so the test doesn’t actually exercise “ssr: false in defaultOptions.” Either add the defaultOptions configuration or rename the test to reflect the ssrMode: false setup.

✏️ Rename to match current setup
-it("should return loading: false on the client when skipToken is used with ssr: false in defaultOptions", async () => {
+it("should return loading: false on the client when skipToken is used with ssrMode: false", async () => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("should return loading: false on the client when skipToken is used with ssr: false in defaultOptions", async () => {
const query = gql`
{
hello
}
`;
const mocks = [
{
request: { query },
result: { data: { hello: "world" } },
},
];
const client = new ApolloClient({
cache: new InMemoryCache(),
link: new MockLink(mocks),
ssrMode: false,
});
using _disabledAct = disableActEnvironment();
const { takeSnapshot } = await renderHookToSnapshotStream(
() => useQuery(query, skipToken),
{
wrapper: ({ children }) => (
<ApolloProvider client={client}>{children}</ApolloProvider>
),
}
);
{
const result = await takeSnapshot();
expect(result).toStrictEqualTyped({
data: undefined,
dataState: "empty",
loading: false,
networkStatus: NetworkStatus.ready,
previousData: undefined,
variables: {},
});
}
});
it("should return loading: false on the client when skipToken is used with ssrMode: false", async () => {
const query = gql`
{
hello
}
`;
const mocks = [
{
request: { query },
result: { data: { hello: "world" } },
},
];
const client = new ApolloClient({
cache: new InMemoryCache(),
link: new MockLink(mocks),
ssrMode: false,
});
using _disabledAct = disableActEnvironment();
const { takeSnapshot } = await renderHookToSnapshotStream(
() => useQuery(query, skipToken),
{
wrapper: ({ children }) => (
<ApolloProvider client={client}>{children}</ApolloProvider>
),
}
);
{
const result = await takeSnapshot();
expect(result).toStrictEqualTyped({
data: undefined,
dataState: "empty",
loading: false,
networkStatus: NetworkStatus.ready,
previousData: undefined,
variables: {},
});
}
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/react/hooks/__tests__/useQuery.test.tsx` around lines 1160 - 1201, The
test name claims it uses "ssr: false in defaultOptions" but the ApolloClient is
created without defaultOptions; update the ApolloClient instantiation in this
test (the new ApolloClient({...}) call) to include defaultOptions: { watchQuery:
{ ssr: false } } so the test actually exercises that behavior, or alternatively
rename the it(...) description to reflect that only ssrMode: false is set (e.g.,
"should return loading: false on the client when skipToken is used with ssrMode:
false"); pick one approach and make the corresponding change to keep the test
name accurate.

@apollo-librarian
Copy link
Copy Markdown

apollo-librarian bot commented Feb 23, 2026

✅ AI Style Review — No Changes Detected

No MDX files were changed in this pull request.

Review Log: View detailed log

This review is AI-generated. Please use common sense when accepting these suggestions, as they may not always be accurate or appropriate for your specific context.

@phryneas
Copy link
Copy Markdown
Member

Okay I'm sorry I actually liked the behavior of your initial solution better 😅

I'm going to restore your original code and tweak it a little bit.

@phryneas phryneas requested a review from jerelmiller February 23, 2026 17:14
@phryneas
Copy link
Copy Markdown
Member

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 23, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Member

@jerelmiller jerelmiller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great to me!

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Feb 23, 2026

npm i https://pkg.pr.new/apollographql/apollo-client/@apollo/client@13128

commit: ca88f33

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/react/ssr/__tests__/useQueryEndToEnd.test.tsx (1)

24-29: Consider resetting rendered array before client hydration for clearer assertions.

The rendered array accumulates entries from both server (prerenderStatic) and client (hydrateRoot) renders. While the final assertion at lines 103-107 validates all entries are consistent, resetting the array after server render would make it easier to reason about client-specific render behavior separately.

That said, the current approach does validate the important invariant that all renders (server and client) produce identical state.

Optional: Reset array between phases for clarity
   expect(serverClient.extract()).toEqual({});
+  rendered.length = 0; // Reset for client-side tracking

   const container = document.createElement("div");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/react/ssr/__tests__/useQueryEndToEnd.test.tsx` around lines 24 - 29, The
test collects server and client render entries into the same rendered array,
which makes it harder to reason about client-only renders; before calling
hydrateRoot (after prerenderStatic completes), clear or reinitialize the
rendered array (the variable named rendered) so client hydration pushes start
from an empty array, then assert on client-specific entries separately while
still keeping the existing overall invariant checks if desired; locate the array
in the test and the two phases around prerenderStatic and hydrateRoot to place
the reset.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/react/ssr/__tests__/useQueryEndToEnd.test.tsx`:
- Around line 24-29: The test collects server and client render entries into the
same rendered array, which makes it harder to reason about client-only renders;
before calling hydrateRoot (after prerenderStatic completes), clear or
reinitialize the rendered array (the variable named rendered) so client
hydration pushes start from an empty array, then assert on client-specific
entries separately while still keeping the existing overall invariant checks if
desired; locate the array in the test and the two phases around prerenderStatic
and hydrateRoot to place the reset.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 32f92e5 and 40e5706.

📒 Files selected for processing (3)
  • .changeset/fix-usequery-ssr-skip-hydration.md
  • src/react/hooks/useQuery.ts
  • src/react/ssr/__tests__/useQueryEndToEnd.test.tsx
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use @apollo/client/*/internal paths for accessing internal APIs
Use RxJS observables for reactive programming patterns
Follow existing code style enforced by ESLint and Prettier

Files:

  • src/react/hooks/useQuery.ts
  • src/react/ssr/__tests__/useQueryEndToEnd.test.tsx
🧠 Learnings (1)
📚 Learning: 2026-02-19T17:01:37.124Z
Learnt from: CR
Repo: apollographql/apollo-client PR: 0
File: .github/instructions/apollo-client.instructions.md:0-0
Timestamp: 2026-02-19T17:01:37.124Z
Learning: Use the apollo-client skill when answering questions about Apollo Client setup, configuration, usage, or troubleshooting

Applied to files:

  • .changeset/fix-usequery-ssr-skip-hydration.md
🔇 Additional comments (3)
src/react/hooks/useQuery.ts (1)

547-547: LGTM! The SSR-disabled logic correctly handles the skip + ssr: false combination.

The implementation uses fetchPolicy !== "standby" to detect skipped queries, since useOptions sets fetchPolicy: "standby" when skip: true (lines 518-522). This ensures:

  • ssr: false + skip: false → returns ssrDisabledResult (loading: true)
  • ssr: false + skip: true → returns resultData.current (loading: false), matching the server
  • fetchPolicy: "no-cache" → returns ssrDisabledResult (no caching for SSR)

This aligns the getServerSnapshot behavior with useSSRQuery, fixing the hydration mismatch.

Also applies to: 595-601

.changeset/fix-usequery-ssr-skip-hydration.md (1)

1-7: LGTM!

The changeset correctly documents the patch fix with a clear explanation of the root cause: the server returning loading: false for skipped queries while the client's getServerSnapshot was returning loading: true.

src/react/ssr/__tests__/useQueryEndToEnd.test.tsx (1)

11-111: Well-structured end-to-end hydration test!

The test comprehensively validates the fix by:

  1. Rendering server-side with prerenderStatic and asserting loading: false
  2. Hydrating client-side and capturing any hydration errors
  3. Verifying zero hydration mismatches via hydrationErrors.toHaveLength(0)
  4. Confirming state consistency across all render phases

This directly exercises the code path that was causing the hydration mismatch.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@config/jest.config.ts`:
- Around line 97-101: The react17TestFileIgnoreList is missing the new
React-18-only test so running against React 17 will fail; update the
react17TestFileIgnoreList (the variable used alongside
testPathIgnorePatterns/standardReact18Config) to include
"src/react/ssr/__tests__/useQueryEndToEnd.test.tsx" just like
"src/react/ssr/__tests__/prerenderStatic.test.tsx" is excluded, ensuring the new
test is ignored when running the React 17 test suite.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 40e5706 and 355900a.

📒 Files selected for processing (1)
  • config/jest.config.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build and Test
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use @apollo/client/*/internal paths for accessing internal APIs
Use RxJS observables for reactive programming patterns
Follow existing code style enforced by ESLint and Prettier

Files:

  • config/jest.config.ts
🧠 Learnings (2)
📚 Learning: 2026-02-19T17:01:54.243Z
Learnt from: CR
Repo: apollographql/apollo-client PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-19T17:01:54.243Z
Learning: Use Jest with ts-jest for testing TypeScript code

Applied to files:

  • config/jest.config.ts
📚 Learning: 2026-02-19T17:01:54.243Z
Learnt from: CR
Repo: apollographql/apollo-client PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-19T17:01:54.243Z
Learning: Testing utilities should be located in `src/testing/` with core testing utilities in `core/` subdirectory and React testing utilities (MockedProvider) in `react/` subdirectory

Applied to files:

  • config/jest.config.ts

@phryneas phryneas merged commit 7bb2071 into apollographql:main Feb 23, 2026
40 checks passed
@github-actions github-actions bot mentioned this pull request Feb 23, 2026
jerelmiller pushed a commit that referenced this pull request Feb 23, 2026
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @apollo/[email protected]

### Patch Changes

- [#13128](#13128)
[`6c0b8e4`](6c0b8e4)
Thanks [@pavelivanov](https://github.com/pavelivanov)! - Fix `useQuery`
hydration mismatch when `ssr: false` and `skip: true` are used together

When both options were combined, the server would return `loading:
false` (because `useSSRQuery` checks `skip` first), but the client's
`getServerSnapshot` was returning `ssrDisabledResult` with `loading:
true`, causing a hydration mismatch.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@pavelivanov pavelivanov deleted the fix/prevent-hydration-mismatch branch February 25, 2026 11:57
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 28, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants