Skip to content

Commit bb6ae01

Browse files
committed
feat(react-query): add pause provider
1 parent 8c03bd0 commit bb6ae01

16 files changed

+326
-9
lines changed

examples/react/pause/.eslintrc

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": ["plugin:react/jsx-runtime", "plugin:react-hooks/recommended"]
3+
}

examples/react/pause/.gitignore

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# production
12+
/build
13+
14+
pnpm-lock.yaml
15+
yarn.lock
16+
package-lock.json
17+
18+
# misc
19+
.DS_Store
20+
.env.local
21+
.env.development.local
22+
.env.test.local
23+
.env.production.local
24+
25+
npm-debug.log*
26+
yarn-debug.log*
27+
yarn-error.log*

examples/react/pause/README.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Example
2+
3+
To run this example:
4+
5+
- `npm install`
6+
- `npm run dev`

examples/react/pause/index.html

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<link rel="icon" type="image/svg+xml" href="/emblem-light.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1" />
7+
<meta name="theme-color" content="#000000" />
8+
9+
<title>TanStack Query React Pause Example App</title>
10+
</head>
11+
<body>
12+
<noscript>You need to enable JavaScript to run this app.</noscript>
13+
<div id="root"></div>
14+
<script type="module" src="/src/index.tsx"></script>
15+
</body>
16+
</html>

examples/react/pause/package.json

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "@tanstack/query-example-react-pause",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"dev": "vite",
7+
"build": "vite build",
8+
"preview": "vite preview"
9+
},
10+
"dependencies": {
11+
"@tanstack/react-query": "^5.67.1",
12+
"@tanstack/react-query-devtools": "^5.67.1",
13+
"react": "^19.0.0",
14+
"react-dom": "^19.0.0"
15+
},
16+
"devDependencies": {
17+
"@vitejs/plugin-react": "^4.3.3",
18+
"typescript": "5.8.2",
19+
"vite": "^5.3.5"
20+
}
21+
}
Loading

examples/react/pause/src/index.tsx

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import ReactDOM from 'react-dom/client'
2+
import {
3+
PauseManager,
4+
PauseManagerProvider,
5+
QueryClient,
6+
QueryClientProvider,
7+
useQuery,
8+
} from '@tanstack/react-query'
9+
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
10+
import { memo, useRef, useSyncExternalStore } from 'react'
11+
12+
const queryClient = new QueryClient()
13+
const pauseManager = new PauseManager(true)
14+
15+
let counter = 1
16+
17+
const useCounter = () =>
18+
useQuery({
19+
queryKey: ['counter'],
20+
refetchOnMount: false,
21+
queryFn: () => counter++,
22+
})
23+
24+
export default function App() {
25+
return (
26+
<QueryClientProvider client={queryClient}>
27+
<ReactQueryDevtools />
28+
<Example />
29+
</QueryClientProvider>
30+
)
31+
}
32+
33+
function Example() {
34+
const { data, refetch } = useCounter()
35+
const isPaused = useSyncExternalStore(
36+
(onStoreChange) => pauseManager.subscribe(onStoreChange),
37+
() => pauseManager.isPaused(),
38+
)
39+
40+
return (
41+
<div>
42+
<p>Parent Counter: {data}</p>
43+
<div style={{ opacity: isPaused ? 0.5 : 1 }}>
44+
<PauseManagerProvider pauseManager={pauseManager}>
45+
<MemoisedChild />
46+
</PauseManagerProvider>
47+
</div>
48+
<button onClick={() => refetch()}>Increment</button>
49+
<button onClick={() => pauseManager.setPaused(!isPaused)}>
50+
{isPaused ? 'Resume' : 'Pause'}
51+
</button>
52+
</div>
53+
)
54+
}
55+
56+
const MemoisedChild = memo(() => {
57+
const { data } = useCounter()
58+
const renders = useRef(0)
59+
renders.current++
60+
61+
return (
62+
<>
63+
<p>Child counter: {data}</p>
64+
<p>Child renders: {renders.current}</p>
65+
</>
66+
)
67+
})
68+
69+
const rootElement = document.getElementById('root') as HTMLElement
70+
ReactDOM.createRoot(rootElement).render(<App />)

examples/react/pause/tsconfig.json

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2020",
4+
"useDefineForClassFields": true,
5+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
6+
"module": "ESNext",
7+
"skipLibCheck": true,
8+
9+
/* Bundler mode */
10+
"moduleResolution": "Bundler",
11+
"allowImportingTsExtensions": true,
12+
"resolveJsonModule": true,
13+
"isolatedModules": true,
14+
"noEmit": true,
15+
"jsx": "react-jsx",
16+
17+
/* Linting */
18+
"strict": true,
19+
"noUnusedLocals": true,
20+
"noUnusedParameters": true,
21+
"noFallthroughCasesInSwitch": true
22+
},
23+
"include": ["src", "eslint.config.js"]
24+
}

examples/react/pause/vite.config.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { defineConfig } from 'vite'
2+
import react from '@vitejs/plugin-react'
3+
4+
export default defineConfig({
5+
plugins: [react()],
6+
})

packages/query-core/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export { MutationObserver } from './mutationObserver'
1313
export { notifyManager } from './notifyManager'
1414
export { focusManager } from './focusManager'
1515
export { onlineManager } from './onlineManager'
16+
export { PauseManager } from './pauseManager'
1617
export {
1718
hashKey,
1819
replaceEqualDeep,
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Subscribable } from './subscribable'
2+
3+
type Listener = (paused: boolean) => void
4+
5+
export class PauseManager extends Subscribable<Listener> {
6+
#paused: boolean
7+
8+
constructor(paused = false) {
9+
super()
10+
this.#paused = paused
11+
}
12+
13+
#onChange(): void {
14+
const isPaused = this.isPaused()
15+
this.listeners.forEach((listener) => {
16+
listener(isPaused)
17+
})
18+
}
19+
20+
setPaused(paused: boolean): void {
21+
const changed = this.#paused !== paused
22+
if (changed) {
23+
this.#paused = paused
24+
this.#onChange()
25+
}
26+
}
27+
28+
isPaused(): boolean {
29+
return this.#paused
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use client'
2+
import * as React from 'react'
3+
4+
import type { PauseManager } from '@tanstack/query-core'
5+
6+
export const PauseManagerContext = React.createContext<
7+
PauseManager | undefined
8+
>(undefined)
9+
10+
export const usePauseManager = () => {
11+
return React.useContext(PauseManagerContext)
12+
}
13+
14+
export type PauseManagerProviderProps = {
15+
pauseManager: PauseManager
16+
children?: React.ReactNode
17+
}
18+
19+
export const PauseManagerProvider = ({
20+
pauseManager,
21+
children,
22+
}: PauseManagerProviderProps): React.JSX.Element => {
23+
return (
24+
<PauseManagerContext.Provider value={pauseManager}>
25+
{children}
26+
</PauseManagerContext.Provider>
27+
)
28+
}

packages/react-query/src/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ export type {
2929
UndefinedInitialDataInfiniteOptions,
3030
UnusedSkipTokenInfiniteOptions,
3131
} from './infiniteQueryOptions'
32+
export {
33+
PauseManagerContext,
34+
PauseManagerProvider,
35+
usePauseManager,
36+
} from './PauseManagerProvider'
3237
export {
3338
QueryClientContext,
3439
QueryClientProvider,

packages/react-query/src/useBaseQuery.ts

+26-4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
willFetch,
1818
} from './suspense'
1919
import { noop } from './utils'
20+
import { usePauseManager } from './PauseManagerProvider'
2021
import type {
2122
QueryClient,
2223
QueryKey,
@@ -53,6 +54,7 @@ export function useBaseQuery<
5354
const client = useQueryClient(queryClient)
5455
const isRestoring = useIsRestoring()
5556
const errorResetBoundary = useQueryErrorResetBoundary()
57+
const pauseManager = usePauseManager()
5658
const defaultedOptions = client.defaultQueryOptions(options)
5759

5860
;(client.getDefaultOptions().queries as any)?._experimental_beforeQuery?.(
@@ -97,17 +99,37 @@ export function useBaseQuery<
9799
React.useSyncExternalStore(
98100
React.useCallback(
99101
(onStoreChange) => {
100-
const unsubscribe = shouldSubscribe
101-
? observer.subscribe(notifyManager.batchCalls(onStoreChange))
102-
: noop
102+
if (!shouldSubscribe) {
103+
return noop
104+
}
105+
106+
const subscribe = () =>
107+
observer.subscribe(notifyManager.batchCalls(onStoreChange))
108+
let unsubscribe: ReturnType<typeof subscribe>
109+
110+
if (pauseManager) {
111+
let unsubscribeObserver: (() => void) | undefined
112+
const onPausedChange = (paused: boolean) => {
113+
unsubscribeObserver?.()
114+
unsubscribeObserver = paused ? undefined : subscribe()
115+
}
116+
const unsubscribeVisibility = pauseManager.subscribe(onPausedChange)
117+
onPausedChange(pauseManager.isPaused())
118+
unsubscribe = () => {
119+
unsubscribeVisibility()
120+
unsubscribeObserver?.()
121+
}
122+
} else {
123+
unsubscribe = subscribe()
124+
}
103125

104126
// Update result to make sure we did not miss any query updates
105127
// between creating the observer and subscribing to it.
106128
observer.updateResult()
107129

108130
return unsubscribe
109131
},
110-
[observer, shouldSubscribe],
132+
[observer, shouldSubscribe, pauseManager],
111133
),
112134
() => observer.getCurrentResult(),
113135
() => observer.getCurrentResult(),

packages/react-query/src/useQueries.ts

+24-5
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
willFetch,
2222
} from './suspense'
2323
import { noop } from './utils'
24+
import { usePauseManager } from './PauseManagerProvider'
2425
import type {
2526
DefinedUseQueryResult,
2627
UseQueryOptions,
@@ -224,6 +225,7 @@ export function useQueries<
224225
const client = useQueryClient(queryClient)
225226
const isRestoring = useIsRestoring()
226227
const errorResetBoundary = useQueryErrorResetBoundary()
228+
const pauseManager = usePauseManager()
227229

228230
const defaultedQueries = React.useMemo(
229231
() =>
@@ -268,11 +270,28 @@ export function useQueries<
268270
const shouldSubscribe = !isRestoring && options.subscribed !== false
269271
React.useSyncExternalStore(
270272
React.useCallback(
271-
(onStoreChange) =>
272-
shouldSubscribe
273-
? observer.subscribe(notifyManager.batchCalls(onStoreChange))
274-
: noop,
275-
[observer, shouldSubscribe],
273+
(onStoreChange) => {
274+
if (!shouldSubscribe) {
275+
return noop
276+
}
277+
const subscribe = () =>
278+
observer.subscribe(notifyManager.batchCalls(onStoreChange))
279+
if (pauseManager) {
280+
let unsubscribeObserver: (() => void) | undefined
281+
const onPausedChange = (paused: boolean) => {
282+
unsubscribeObserver?.()
283+
unsubscribeObserver = paused ? undefined : subscribe()
284+
}
285+
const unsubscribeVisibility = pauseManager.subscribe(onPausedChange)
286+
onPausedChange(pauseManager.isPaused())
287+
return () => {
288+
unsubscribeVisibility()
289+
unsubscribeObserver?.()
290+
}
291+
}
292+
return subscribe()
293+
},
294+
[observer, shouldSubscribe, pauseManager],
276295
),
277296
() => observer.getCurrentResult(),
278297
() => observer.getCurrentResult(),

0 commit comments

Comments
 (0)