Skip to content

feat/environmentManager#10199

Open
TkDodo wants to merge 6 commits intomainfrom
feature/environmentManager
Open

feat/environmentManager#10199
TkDodo wants to merge 6 commits intomainfrom
feature/environmentManager

Conversation

@TkDodo
Copy link
Collaborator

@TkDodo TkDodo commented Feb 28, 2026

alternative fix for #10139

Summary by CodeRabbit

  • New Features

    • Introduced environmentManager as a new public API for runtime server-detection management, allowing queries to check and override server environment status.
    • Added isServer() and setIsServer() methods for flexible server-state control.
  • Documentation

    • Added comprehensive documentation for environmentManager with usage examples and configuration guidance.
  • Deprecations

    • Marked direct isServer export as deprecated in favor of environmentManager.isServer().

@changeset-bot
Copy link

changeset-bot bot commented Feb 28, 2026

⚠️ No Changeset found

Latest commit: 4206f79

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 28, 2026

📝 Walkthrough

Walkthrough

This change introduces a new environmentManager singleton that centralizes server-detection logic across the codebase. Direct isServer imports are replaced with calls to environmentManager.isServer(), and the old isServer export is deprecated. Tests can now override server state via environmentManager.setIsServer() instead of monkey-patching utilities.

Changes

Cohort / File(s) Summary
Documentation
docs/config.json, docs/reference/environmentManager.md
Adds API reference entry and comprehensive documentation for the new environmentManager, including usage examples and method descriptions.
Core Manager Implementation
packages/query-core/src/environmentManager.ts, packages/query-core/src/index.ts, packages/query-core/src/utils.ts
Introduces new environmentManager singleton with isServer() and setIsServer() methods; exports new manager; adds deprecation notice to legacy isServer export.
Manager Updates
packages/query-core/src/focusManager.ts, packages/query-core/src/onlineManager.ts, packages/query-core/src/queryObserver.ts, packages/query-core/src/removable.ts, packages/query-core/src/retryer.ts
Replaces direct isServer checks with environmentManager.isServer() calls; updates imports accordingly.
Framework Integrations
packages/react-query/src/useBaseQuery.ts, packages/preact-query/src/useBaseQuery.ts
Replaces isServer import with environmentManager; updates prefetch-in-render server detection to use environmentManager.isServer().
Test Utilities & Infrastructure
packages/query-core/src/__tests__/utils.ts, packages/react-query/src/__tests__/utils.tsx, packages/preact-query/src/__tests__/utils.tsx
Refactors setIsServer helpers to delegate to environmentManager.setIsServer() instead of direct utility patching; updates cleanup behavior.
Test Coverage
packages/query-core/src/__tests__/environmentManager.test.tsx, packages/query-core/src/__tests__/focusManager.test.tsx, packages/query-core/src/__tests__/onlineManager.test.tsx
Adds new environmentManager tests; refactors existing manager tests to use window spy mocking instead of server-state helpers.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • arnoud-dv

Poem

🐰 A manager springs forth with care,
Server-detection goes everywhere!
No more patching in the tests we weave,
Just setIsServer and we believe,
Centralized logic, clean and bright! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is minimal and does not follow the template's structure. It lacks a detailed '🎯 Changes' section explaining the feature, missing checklist confirmations, and lacks a '🚀 Release Impact' section addressing changeset requirements. Expand the description with detailed change summary, check the contribution and testing requirements, and clarify whether a changeset was generated for this published feature.
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 (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat/environmentManager' directly identifies the main feature being introduced—a new environmentManager utility for server-detection management across the codebase.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/environmentManager

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

@nx-cloud
Copy link

nx-cloud bot commented Feb 28, 2026

View your CI Pipeline Execution ↗ for commit 4206f79

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 4m 53s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 2s View ↗

☁️ Nx Cloud last updated this comment at 2026-02-28 16:04:25 UTC

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 28, 2026

More templates

@tanstack/angular-query-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-experimental@10199

@tanstack/eslint-plugin-query

npm i https://pkg.pr.new/@tanstack/eslint-plugin-query@10199

@tanstack/preact-query

npm i https://pkg.pr.new/@tanstack/preact-query@10199

@tanstack/query-async-storage-persister

npm i https://pkg.pr.new/@tanstack/query-async-storage-persister@10199

@tanstack/query-broadcast-client-experimental

npm i https://pkg.pr.new/@tanstack/query-broadcast-client-experimental@10199

@tanstack/query-core

npm i https://pkg.pr.new/@tanstack/query-core@10199

@tanstack/query-devtools

npm i https://pkg.pr.new/@tanstack/query-devtools@10199

@tanstack/query-persist-client-core

npm i https://pkg.pr.new/@tanstack/query-persist-client-core@10199

@tanstack/query-sync-storage-persister

npm i https://pkg.pr.new/@tanstack/query-sync-storage-persister@10199

@tanstack/react-query

npm i https://pkg.pr.new/@tanstack/react-query@10199

@tanstack/react-query-devtools

npm i https://pkg.pr.new/@tanstack/react-query-devtools@10199

@tanstack/react-query-next-experimental

npm i https://pkg.pr.new/@tanstack/react-query-next-experimental@10199

@tanstack/react-query-persist-client

npm i https://pkg.pr.new/@tanstack/react-query-persist-client@10199

@tanstack/solid-query

npm i https://pkg.pr.new/@tanstack/solid-query@10199

@tanstack/solid-query-devtools

npm i https://pkg.pr.new/@tanstack/solid-query-devtools@10199

@tanstack/solid-query-persist-client

npm i https://pkg.pr.new/@tanstack/solid-query-persist-client@10199

@tanstack/svelte-query

npm i https://pkg.pr.new/@tanstack/svelte-query@10199

@tanstack/svelte-query-devtools

npm i https://pkg.pr.new/@tanstack/svelte-query-devtools@10199

@tanstack/svelte-query-persist-client

npm i https://pkg.pr.new/@tanstack/svelte-query-persist-client@10199

@tanstack/vue-query

npm i https://pkg.pr.new/@tanstack/vue-query@10199

@tanstack/vue-query-devtools

npm i https://pkg.pr.new/@tanstack/vue-query-devtools@10199

commit: 7233c8c

@github-actions
Copy link
Contributor

github-actions bot commented Feb 28, 2026

size-limit report 📦

Path Size
react full 11.96 KB (+0.37% 🔺)
react minimal 8.98 KB (+0.35% 🔺)

@TkDodo TkDodo marked this pull request as ready for review February 28, 2026 15:41
Copy link
Contributor

@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: 2

🧹 Nitpick comments (3)
packages/vue-query/src/__tests__/queryClient.test.ts (1)

23-23: This change appears unrelated to the environmentManager feature.

The type improvement for the filter predicate is a good change, but it doesn't seem related to the environmentManager feature being introduced in this PR. Consider whether this should be in a separate PR for cleaner history, or note in the PR description that this is an incidental type improvement.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vue-query/src/__tests__/queryClient.test.ts` at line 23, The change
to the filter predicate type guard in queryClient.test.ts (the line casting to
QueryClientPrototypeMethod) is unrelated to the environmentManager feature;
either revert that type-only change from this branch and leave the
environmentManager PR focused, or extract the type improvement into its own
commit/branch and open a separate PR; update the current PR description to
mention the incidental type tweak if you intend to keep it here. Ensure you
reference the test file's filter predicate that narrows to
QueryClientPrototypeMethod when making the change.
packages/query-core/src/__tests__/environmentManager.test.tsx (1)

21-24: Consider consolidating or differentiating this test.

This test ("should allow overriding isServer with a function") is essentially identical to the first part of the test on lines 13-15. Both tests call setIsServer(() => true) and assert the result. The test name suggests testing "with a function" but that's already what the API requires—setIsServer only accepts a function per the type IsServerValue = () => boolean.

Consider either removing this test as it's redundant, or differentiating it by testing a more dynamic function (e.g., one that returns different values based on some condition).

Example of a more meaningful test
  test('should allow overriding isServer with a function', () => {
-   environmentManager.setIsServer(() => true)
-   expect(environmentManager.isServer()).toBe(true)
+   let value = true
+   environmentManager.setIsServer(() => value)
+   expect(environmentManager.isServer()).toBe(true)
+   
+   value = false
+   expect(environmentManager.isServer()).toBe(false)
  })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/query-core/src/__tests__/environmentManager.test.tsx` around lines
21 - 24, The test is redundant because environmentManager.setIsServer(() =>
true) is already asserted earlier; either remove this duplicate test or change
it to exercise a dynamic function: replace the static () => true with a function
that flips or returns different values based on external state, then assert
environmentManager.isServer() reflects that changing state (use
environmentManager.setIsServer and environmentManager.isServer to locate code to
modify). Ensure the new test verifies the dynamic behavior across state changes
rather than repeating the same static assertion.
packages/query-core/src/onlineManager.ts (1)

17-17: Inconsistent with environmentManager pattern used elsewhere.

This change uses typeof window !== 'undefined' directly instead of environmentManager.isServer(). While this works for detecting the browser environment, it creates an inconsistency with the rest of the codebase migration:

  • If a user calls environmentManager.setIsServer(() => true) to simulate server-side behavior (e.g., for testing), the OnlineManager will still register window event listeners because window exists.
  • Other files (retryer.ts, queryObserver.ts, removable.ts) use environmentManager.isServer() consistently.

If the intent is to check specifically for window.addEventListener availability (as the comment suggests for React Native compatibility), the current approach is fine. However, if the goal is to respect the environmentManager override, consider:

Option: Use environmentManager for consistency
-      if (typeof window !== 'undefined' && window.addEventListener) {
+      if (!environmentManager.isServer() && typeof window !== 'undefined' && window.addEventListener) {

This would require adding the import:

+import { environmentManager } from './environmentManager'

Please clarify the intended behavior: should OnlineManager respect environmentManager.setIsServer() overrides, or is checking for window existence the correct approach here?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/query-core/src/onlineManager.ts` at line 17, Replace the direct
"typeof window !== 'undefined' && window.addEventListener" check in
OnlineManager with the environmentManager-based guard so the module respects
environmentManager.setIsServer() overrides; import environmentManager and change
the condition to "!environmentManager.isServer() && typeof window !==
'undefined' && window.addEventListener" (so you still guard for environments
like React Native while honoring test/server overrides) and update any
surrounding comment to reflect the combined intent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/query-core/src/__tests__/focusManager.test.tsx`:
- Around line 63-68: The test currently calls subscribe twice (once via
expect(subscribe).not.toThrow() which invokes subscribe, and again on the next
line) but only calls unsubscribe once, leaving a dangling listener; change the
test so both subscriptions are unsubscribed: capture the first unsubscribe by
invoking subscribe inside the expect (e.g. let firstUnsub; expect(() => {
firstUnsub = subscribe(); }).not.toThrow()), then call const secondUnsub =
subscribe() and finally call firstUnsub() and secondUnsub(); reference the
subscribe constant and focusManager.subscribe to locate the code and ensure
removeEventListener gets exercised.

In `@packages/query-core/src/__tests__/onlineManager.test.tsx`:
- Around line 72-77: The test currently calls onlineManager.subscribe twice
(once via expect(subscribe).not.toThrow() and again when assigning unsubscribe)
which leaves the first listener active and masks cleanup; fix by calling
subscribe only once: replace the expect + second call with a single call to
onlineManager.subscribe(() => undefined) assigned to unsubscribe, then assert
the returned unsubscribe is a function (e.g.,
expect(unsubscribe).toBeInstanceOf(Function)) and finally call unsubscribe() so
onUnsubscribe() can be exercised; refer to onlineManager.subscribe, unsubscribe,
and onUnsubscribe to locate the code to change.

---

Nitpick comments:
In `@packages/query-core/src/__tests__/environmentManager.test.tsx`:
- Around line 21-24: The test is redundant because
environmentManager.setIsServer(() => true) is already asserted earlier; either
remove this duplicate test or change it to exercise a dynamic function: replace
the static () => true with a function that flips or returns different values
based on external state, then assert environmentManager.isServer() reflects that
changing state (use environmentManager.setIsServer and
environmentManager.isServer to locate code to modify). Ensure the new test
verifies the dynamic behavior across state changes rather than repeating the
same static assertion.

In `@packages/query-core/src/onlineManager.ts`:
- Line 17: Replace the direct "typeof window !== 'undefined' &&
window.addEventListener" check in OnlineManager with the
environmentManager-based guard so the module respects
environmentManager.setIsServer() overrides; import environmentManager and change
the condition to "!environmentManager.isServer() && typeof window !==
'undefined' && window.addEventListener" (so you still guard for environments
like React Native while honoring test/server overrides) and update any
surrounding comment to reflect the combined intent.

In `@packages/vue-query/src/__tests__/queryClient.test.ts`:
- Line 23: The change to the filter predicate type guard in queryClient.test.ts
(the line casting to QueryClientPrototypeMethod) is unrelated to the
environmentManager feature; either revert that type-only change from this branch
and leave the environmentManager PR focused, or extract the type improvement
into its own commit/branch and open a separate PR; update the current PR
description to mention the incidental type tweak if you intend to keep it here.
Ensure you reference the test file's filter predicate that narrows to
QueryClientPrototypeMethod when making the change.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0da627b and 9ee18df.

📒 Files selected for processing (19)
  • docs/config.json
  • docs/reference/environmentManager.md
  • packages/preact-query/src/__tests__/utils.tsx
  • packages/preact-query/src/useBaseQuery.ts
  • packages/query-core/src/__tests__/environmentManager.test.tsx
  • packages/query-core/src/__tests__/focusManager.test.tsx
  • packages/query-core/src/__tests__/onlineManager.test.tsx
  • packages/query-core/src/__tests__/utils.ts
  • packages/query-core/src/environmentManager.ts
  • packages/query-core/src/focusManager.ts
  • packages/query-core/src/index.ts
  • packages/query-core/src/onlineManager.ts
  • packages/query-core/src/queryObserver.ts
  • packages/query-core/src/removable.ts
  • packages/query-core/src/retryer.ts
  • packages/query-core/src/utils.ts
  • packages/react-query/src/__tests__/utils.tsx
  • packages/react-query/src/useBaseQuery.ts
  • packages/vue-query/src/__tests__/queryClient.test.ts

Copy link
Contributor

@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 (2)
packages/query-core/src/__tests__/focusManager.test.tsx (1)

57-60: Use try/finally for spy restoration to prevent cross-test leakage.

If an assertion fails before Line 76, the mocked globalThis.window getter remains active and can pollute following tests.

Suggested hardening
   test('cleanup (removeEventListener) should not be called if window is not defined', () => {
     const windowSpy = vi.spyOn(globalThis, 'window', 'get')
     windowSpy.mockImplementation(
       () => undefined as unknown as Window & typeof globalThis,
     )
     const removeEventListenerSpy = vi.spyOn(globalThis, 'removeEventListener')

-    const subscribe = () => focusManager.subscribe(() => undefined)
-    let firstUnsubscribe: (() => void) | undefined
-
-    expect(() => {
-      firstUnsubscribe = subscribe()
-    }).not.toThrow()
-    const secondUnsubscribe = subscribe()
-
-    firstUnsubscribe?.()
-    secondUnsubscribe()
-
-    expect(removeEventListenerSpy).not.toHaveBeenCalled()
-
-    windowSpy.mockRestore()
+    try {
+      const subscribe = () => focusManager.subscribe(() => undefined)
+      let firstUnsubscribe: (() => void) | undefined
+
+      expect(() => {
+        firstUnsubscribe = subscribe()
+      }).not.toThrow()
+      const secondUnsubscribe = subscribe()
+
+      firstUnsubscribe?.()
+      secondUnsubscribe()
+
+      expect(removeEventListenerSpy).not.toHaveBeenCalled()
+    } finally {
+      removeEventListenerSpy.mockRestore()
+      windowSpy.mockRestore()
+    }
   })

Also applies to: 76-76

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/query-core/src/__tests__/focusManager.test.tsx` around lines 57 -
60, Wrap the test code that mocks globalThis.window (the windowSpy created via
vi.spyOn(globalThis, 'window', 'get')) in a try/finally block so you always
restore the spy; move windowSpy.mockImplementation(...) into the try and call
windowSpy.mockRestore() in the finally to ensure the mocked getter is removed
even if assertions fail (apply the same pattern for the other instance around
line 76).
packages/query-core/src/__tests__/onlineManager.test.tsx (1)

66-69: Guard spy restoration with finally for deterministic test cleanup.

If anything fails before Line 79, the mocked window getter may leak into later tests.

Suggested hardening
   test('cleanup (removeEventListener) should not be called if window is not defined', () => {
     const windowSpy = vi.spyOn(globalThis, 'window', 'get')
     windowSpy.mockImplementation(
       () => undefined as unknown as Window & typeof globalThis,
     )
     const removeEventListenerSpy = vi.spyOn(globalThis, 'removeEventListener')

-    const unsubscribe = onlineManager.subscribe(() => undefined)
-    expect(unsubscribe).toBeInstanceOf(Function)
-
-    unsubscribe()
-
-    expect(removeEventListenerSpy).not.toHaveBeenCalled()
-
-    windowSpy.mockRestore()
+    try {
+      const unsubscribe = onlineManager.subscribe(() => undefined)
+      expect(unsubscribe).toBeInstanceOf(Function)
+      unsubscribe()
+      expect(removeEventListenerSpy).not.toHaveBeenCalled()
+    } finally {
+      removeEventListenerSpy.mockRestore()
+      windowSpy.mockRestore()
+    }
   })

Also applies to: 79-79

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/query-core/src/__tests__/onlineManager.test.tsx` around lines 66 -
69, The test creates a spy via vi.spyOn(globalThis, 'window', 'get') assigned to
windowSpy and mocks it with windowSpy.mockImplementation(...); ensure the spy is
always restored to avoid leaking into other tests by wrapping the test body so
that windowSpy.mockRestore() is called in a finally block (or register
windowSpy.mockRestore() in an afterEach/teardown hook) — update the test that
uses windowSpy, mockImplementation, and windowSpy.mockRestore() to guarantee
restoration even if assertions throw.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/query-core/src/__tests__/focusManager.test.tsx`:
- Around line 57-60: Wrap the test code that mocks globalThis.window (the
windowSpy created via vi.spyOn(globalThis, 'window', 'get')) in a try/finally
block so you always restore the spy; move windowSpy.mockImplementation(...) into
the try and call windowSpy.mockRestore() in the finally to ensure the mocked
getter is removed even if assertions fail (apply the same pattern for the other
instance around line 76).

In `@packages/query-core/src/__tests__/onlineManager.test.tsx`:
- Around line 66-69: The test creates a spy via vi.spyOn(globalThis, 'window',
'get') assigned to windowSpy and mocks it with
windowSpy.mockImplementation(...); ensure the spy is always restored to avoid
leaking into other tests by wrapping the test body so that
windowSpy.mockRestore() is called in a finally block (or register
windowSpy.mockRestore() in an afterEach/teardown hook) — update the test that
uses windowSpy, mockImplementation, and windowSpy.mockRestore() to guarantee
restoration even if assertions throw.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9ee18df and 4206f79.

📒 Files selected for processing (3)
  • packages/query-core/src/__tests__/environmentManager.test.tsx
  • packages/query-core/src/__tests__/focusManager.test.tsx
  • packages/query-core/src/__tests__/onlineManager.test.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/query-core/src/tests/environmentManager.test.tsx

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tanstack Query refetch does not work in certain environments (vscode extension, chrome extension)

1 participant