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: 4 additions & 0 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,10 @@
"label": "onlineManager",
"to": "reference/onlineManager"
},
{
"label": "environmentManager",
"to": "reference/environmentManager"
},
{
"label": "notifyManager",
"to": "reference/notifyManager"
Expand Down
50 changes: 50 additions & 0 deletions docs/reference/environmentManager.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
id: EnvironmentManager
title: environmentManager
---

The `environmentManager` manages how TanStack Query detects whether the current runtime should be treated as server-side.

By default, it uses the same server detection as the exported `isServer` utility from query-core.

Use this manager to override server detection globally for runtimes that are not traditional browser/server environments (for example, extension workers).

Its available methods are:

- [`isServer`](#environmentmanagerisserver)
- [`setIsServer`](#environmentmanagersetisserver)

## `environmentManager.isServer`

Returns whether the current runtime is treated as a server environment.

```tsx
import { environmentManager } from '@tanstack/react-query'

const server = environmentManager.isServer()
```

## `environmentManager.setIsServer`

Overrides the server check globally.

```tsx
import { environmentManager } from '@tanstack/react-query'

// Override
environmentManager.setIsServer(() => {
return typeof window === 'undefined' && !('chrome' in globalThis)
})
```

**Options**

- `isServerValue: () => boolean`

To restore the default behavior, set the function back to query-core's `isServer` utility:

```tsx
import { environmentManager, isServer } from '@tanstack/react-query'

environmentManager.setIsServer(() => isServer)
```
16 changes: 4 additions & 12 deletions packages/preact-query/src/__tests__/utils.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as utils from '@tanstack/query-core'
import { environmentManager, isServer } from '@tanstack/query-core'
import { act, render } from '@testing-library/preact'
import type { ComponentChildren, VNode } from 'preact'
import { useEffect, useState } from 'preact/hooks'
Expand Down Expand Up @@ -58,17 +58,9 @@ export function setActTimeout(fn: () => void, ms?: number) {
}, ms)
}

// This monkey-patches the isServer-value from utils,
// so that we can pretend to be in a server environment
export function setIsServer(isServer: boolean) {
const original = utils.isServer
Object.defineProperty(utils, 'isServer', {
get: () => isServer,
})

export function setIsServer(value: boolean) {
environmentManager.setIsServer(() => value)
return () => {
Object.defineProperty(utils, 'isServer', {
get: () => original,
})
environmentManager.setIsServer(() => isServer)
}
}
4 changes: 2 additions & 2 deletions packages/preact-query/src/useBaseQuery.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isServer, noop, notifyManager } from '@tanstack/query-core'
import { environmentManager, noop, notifyManager } from '@tanstack/query-core'
import type {
QueryClient,
QueryKey,
Expand Down Expand Up @@ -147,7 +147,7 @@ export function useBaseQuery<

if (
defaultedOptions.experimental_prefetchInRender &&
!isServer &&
!environmentManager.isServer() &&
willFetch(result, isRestoring)
) {
const promise = isNewCacheEntry
Expand Down
29 changes: 29 additions & 0 deletions packages/query-core/src/__tests__/environmentManager.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { afterEach, describe, expect, test } from 'vitest'
import { environmentManager, isServer } from '..'

describe('environmentManager', () => {
afterEach(() => {
environmentManager.setIsServer(() => isServer)
})

test('should use the default isServer detection', () => {
expect(environmentManager.isServer()).toBe(isServer)
})

test('should allow overriding isServer globally', () => {
environmentManager.setIsServer(() => true)
expect(environmentManager.isServer()).toBe(true)

environmentManager.setIsServer(() => false)
expect(environmentManager.isServer()).toBe(false)
})

test('should allow overriding isServer with a function', () => {
let server = true
environmentManager.setIsServer(() => server)
expect(environmentManager.isServer()).toBe(true)

server = false
expect(environmentManager.isServer()).toBe(false)
})
})
20 changes: 14 additions & 6 deletions packages/query-core/src/__tests__/focusManager.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest'
import { FocusManager } from '../focusManager'
import { setIsServer } from './utils'

describe('focusManager', () => {
let focusManager: FocusManager
Expand Down Expand Up @@ -55,17 +54,26 @@ describe('focusManager', () => {
})

test('cleanup (removeEventListener) should not be called if window is not defined', () => {
const restoreIsServer = setIsServer(true)

const windowSpy = vi.spyOn(globalThis, 'window', 'get')
windowSpy.mockImplementation(
() => undefined as unknown as Window & typeof globalThis,
)
const removeEventListenerSpy = vi.spyOn(globalThis, 'removeEventListener')

const unsubscribe = focusManager.subscribe(() => undefined)
const subscribe = () => focusManager.subscribe(() => undefined)
let firstUnsubscribe: (() => void) | undefined

unsubscribe()
expect(() => {
firstUnsubscribe = subscribe()
}).not.toThrow()
const secondUnsubscribe = subscribe()

firstUnsubscribe?.()
secondUnsubscribe()

expect(removeEventListenerSpy).not.toHaveBeenCalled()

restoreIsServer()
windowSpy.mockRestore()
})

test('cleanup (removeEventListener) should not be called if window.addEventListener is not defined', () => {
Expand Down
10 changes: 6 additions & 4 deletions packages/query-core/src/__tests__/onlineManager.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { OnlineManager } from '../onlineManager'
import { setIsServer } from './utils'

describe('onlineManager', () => {
let onlineManager: OnlineManager
Expand Down Expand Up @@ -64,17 +63,20 @@ describe('onlineManager', () => {
})

test('cleanup (removeEventListener) should not be called if window is not defined', () => {
const restoreIsServer = setIsServer(true)

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()

restoreIsServer()
windowSpy.mockRestore()
})

test('cleanup (removeEventListener) should not be called if window.addEventListener is not defined', () => {
Expand Down
17 changes: 4 additions & 13 deletions packages/query-core/src/__tests__/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { vi } from 'vitest'
import { onlineManager } from '..'
import * as utils from '../utils'
import { environmentManager, isServer, onlineManager } from '..'
import type { MockInstance } from 'vitest'
import type { MutationOptions, QueryClient } from '..'

Expand All @@ -21,17 +20,9 @@ export function executeMutation<TVariables>(
.execute(variables)
}

// This monkey-patches the isServer-value from utils,
// so that we can pretend to be in a server environment
export function setIsServer(isServer: boolean) {
const original = utils.isServer
Object.defineProperty(utils, 'isServer', {
get: () => isServer,
})

export function setIsServer(value: boolean) {
environmentManager.setIsServer(() => value)
return () => {
Object.defineProperty(utils, 'isServer', {
get: () => original,
})
environmentManager.setIsServer(() => isServer)
}
}
25 changes: 25 additions & 0 deletions packages/query-core/src/environmentManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { isServer } from './utils'

export type IsServerValue = () => boolean

/**
* Manages environment detection used by TanStack Query internals.
*/
export const environmentManager = (() => {
let isServerFn: IsServerValue = () => isServer

return {
/**
* Returns whether the current runtime should be treated as a server environment.
*/
isServer(): boolean {
return isServerFn()
},
/**
* Overrides the server check globally.
*/
setIsServer(isServerValue: IsServerValue): void {
isServerFn = isServerValue
},
}
})()
3 changes: 1 addition & 2 deletions packages/query-core/src/focusManager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Subscribable } from './subscribable'
import { isServer } from './utils'

type Listener = (focused: boolean) => void

Expand All @@ -18,7 +17,7 @@ export class FocusManager extends Subscribable<Listener> {
this.#setup = (onFocus) => {
// addEventListener does not exist in React Native, but window does
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!isServer && window.addEventListener) {
if (typeof window !== 'undefined' && window.addEventListener) {
const listener = () => onFocus()
// Listen to visibilitychange
window.addEventListener('visibilitychange', listener, false)
Expand Down
1 change: 1 addition & 0 deletions packages/query-core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* istanbul ignore file */

export { focusManager } from './focusManager'
export { environmentManager } from './environmentManager'
export {
defaultShouldDehydrateMutation,
defaultShouldDehydrateQuery,
Expand Down
3 changes: 1 addition & 2 deletions packages/query-core/src/onlineManager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Subscribable } from './subscribable'
import { isServer } from './utils'

type Listener = (online: boolean) => void
type SetupFn = (setOnline: Listener) => (() => void) | undefined
Expand All @@ -15,7 +14,7 @@ export class OnlineManager extends Subscribable<Listener> {
this.#setup = (onOnline) => {
// addEventListener does not exist in React Native, but window does
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!isServer && window.addEventListener) {
if (typeof window !== 'undefined' && window.addEventListener) {
const onlineListener = () => onOnline(true)
const offlineListener = () => onOnline(false)
// Listen to online
Expand Down
10 changes: 7 additions & 3 deletions packages/query-core/src/queryObserver.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { focusManager } from './focusManager'
import { environmentManager } from './environmentManager'
import { notifyManager } from './notifyManager'
import { fetchState } from './query'
import { Subscribable } from './subscribable'
import { pendingThenable } from './thenable'
import {
isServer,
isValidTimeout,
noop,
replaceData,
Expand Down Expand Up @@ -358,7 +358,11 @@ export class QueryObserver<
this.#currentQuery,
)

if (isServer || this.#currentResult.isStale || !isValidTimeout(staleTime)) {
if (
environmentManager.isServer() ||
this.#currentResult.isStale ||
!isValidTimeout(staleTime)
) {
return
}

Expand Down Expand Up @@ -389,7 +393,7 @@ export class QueryObserver<
this.#currentRefetchInterval = nextInterval

if (
isServer ||
environmentManager.isServer() ||
resolveEnabled(this.options.enabled, this.#currentQuery) === false ||
!isValidTimeout(this.#currentRefetchInterval) ||
this.#currentRefetchInterval === 0
Expand Down
5 changes: 3 additions & 2 deletions packages/query-core/src/removable.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { timeoutManager } from './timeoutManager'
import { isServer, isValidTimeout } from './utils'
import { environmentManager } from './environmentManager'
import { isValidTimeout } from './utils'
import type { ManagedTimerId } from './timeoutManager'

export abstract class Removable {
Expand All @@ -24,7 +25,7 @@ export abstract class Removable {
// Default to 5 minutes (Infinity for server-side) if no gcTime is set
this.gcTime = Math.max(
this.gcTime || 0,
newGcTime ?? (isServer ? Infinity : 5 * 60 * 1000),
newGcTime ?? (environmentManager.isServer() ? Infinity : 5 * 60 * 1000),
)
}

Expand Down
5 changes: 3 additions & 2 deletions packages/query-core/src/retryer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { focusManager } from './focusManager'
import { onlineManager } from './onlineManager'
import { pendingThenable } from './thenable'
import { isServer, sleep } from './utils'
import { environmentManager } from './environmentManager'
import { sleep } from './utils'
import type { Thenable } from './thenable'
import type { CancelOptions, DefaultError, NetworkMode } from './types'

Expand Down Expand Up @@ -166,7 +167,7 @@ export function createRetryer<TData = unknown, TError = DefaultError>(
}

// Do we need to retry the request?
const retry = config.retry ?? (isServer ? 0 : 3)
const retry = config.retry ?? (environmentManager.isServer() ? 0 : 3)
const retryDelay = config.retryDelay ?? defaultRetryDelay
const delay =
typeof retryDelay === 'function'
Expand Down
3 changes: 3 additions & 0 deletions packages/query-core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ export type QueryTypeFilter = 'all' | 'active' | 'inactive'

// UTILS

/** @deprecated
* use `environmentManager.isServer()` instead.
*/
export const isServer = typeof window === 'undefined' || 'Deno' in globalThis

export function noop(): void
Expand Down
Loading
Loading