Skip to content

Commit 473af3d

Browse files
authored
feat: add refetch on reconnect functionality (TanStack#919)
1 parent 90fb8a4 commit 473af3d

File tree

13 files changed

+160
-60
lines changed

13 files changed

+160
-60
lines changed

docs/src/pages/docs/api.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const {
2929
cacheTime,
3030
keepPreviousData,
3131
refetchOnWindowFocus,
32+
refetchOnReconnect,
3233
refetchInterval,
3334
refetchIntervalInBackground,
3435
queryFnParamsFilter,
@@ -92,6 +93,9 @@ const queryInfo = useQuery({
9293
- `refetchOnWindowFocus: Boolean`
9394
- Optional
9495
- Set this to `true` or `false` to enable/disable automatic refetching on window focus for this query.
96+
- `refetchOnReconnect: Boolean`
97+
- Optional
98+
- Set this to `true` or `false` to enable/disable automatic refetching on reconnect for this query.
9599
- `onSuccess: Function(data) => data`
96100
- Optional
97101
- This function will fire any time the query successfully fetches new data.

docs/src/pages/docs/comparison.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Feature/Capability Key:
3838
| Query Cancellation || 🛑 | 🛑 |
3939
| Partial Query Matching<sup>2</sup> || 🛑 | 🛑 |
4040
| Window Focus Refetching ||| 🛑 |
41-
| Network Status Refetching | 🛑 |||
41+
| Network Status Refetching | |||
4242
| Automatic Refetch after Mutation<sup>3</sup> | 🔶 | 🔶 ||
4343
| Cache Dehydration/Rehydration | 🛑 | 🛑 ||
4444
| React Suspense (Experimental) ||| 🛑 |

docs/src/pages/docs/guides/important-defaults.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ Out of the box, React Query is configured with **aggressive but sane** defaults.
77

88
- Query results that are _currently rendered on the screen_ (via `useQuery` and similar hooks) will become "stale" immediately after they are resolved and will be refetched automatically in the background when they are rendered or used again. To change this, you can alter the default `staleTime` for queries to something other than `0` milliseconds.
99
- Query results that become unused (all instances of the query are unmounted) will still be cached in case they are used again for a default of 5 minutes before they are garbage collected. To change this, you can alter the default `cacheTime` for queries to something other than `1000 * 60 * 5` milliseconds.
10-
- Stale queries will automatically be refetched in the background **when the browser window is refocused by the user**. You can disable this using the `refetchOnWindowFocus` option in queries or the global config.
10+
- Stale queries will automatically be refetched in the background **when the browser window is refocused by the user or when the browser reconnects**. You can disable this using the `refetchOnWindowFocus` and `refetchOnReconnect` options in queries or the global config.
1111
- Queries that fail will silently be retried **3 times, with exponential backoff delay** before capturing and displaying an error to the UI. To change this, you can alter the default `retry` and `retryDelay` options for queries to something other than `3` and the default exponential backoff function.
1212
- Query results by default are structurally shared to detect if data has actually changed and if not, the data reference remains unchanged to better help with value stabilization with regards to useMemo and useCallback. Structural sharing only works with JSON-compatible values, any other value types will always be considered as changed. If you are seeing performance issues because of large responses for example, you can disable this feature with the `config.structuralSharing` flag. If you are dealing with non-JSON compatible values in your query responses and still want to detect if data has changed or not, you can define a data compare function with `config.isDataEqual`.

src/core/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export const DEFAULT_CONFIG: ReactQueryConfig = {
5656
staleTime: 0,
5757
cacheTime: 5 * 60 * 1000,
5858
refetchOnWindowFocus: true,
59+
refetchOnReconnect: true,
5960
refetchOnMount: true,
6061
structuralSharing: true,
6162
},

src/core/index.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
export { queryCache, queryCaches, makeQueryCache } from './queryCache'
22
export { setFocusHandler } from './setFocusHandler'
3-
export {
4-
CancelledError,
5-
deepIncludes,
6-
isCancelledError,
7-
isError,
8-
setConsole,
9-
stableStringify,
10-
} from './utils'
3+
export { setOnlineHandler } from './setOnlineHandler'
4+
export { CancelledError, isCancelledError, isError, setConsole } from './utils'
115

126
// Types
137
export * from './types'

src/core/query.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
isCancelable,
88
isCancelledError,
99
isDocumentVisible,
10+
isOnline,
1011
isServer,
1112
replaceEqualDeep,
1213
sleep,
@@ -208,7 +209,7 @@ export class Query<TResult, TError> {
208209
this.cancelFetch?.()
209210
}
210211

211-
continue(): void {
212+
private continue(): void {
212213
this.continueFetch?.()
213214
}
214215

@@ -280,12 +281,30 @@ export class Query<TResult, TError> {
280281
return this.observers.some(observer => observer.config.enabled)
281282
}
282283

283-
shouldRefetchOnWindowFocus(): boolean {
284-
return (
285-
this.isEnabled() &&
284+
onWindowFocus(): void {
285+
if (
286286
this.state.isStale &&
287-
this.observers.some(observer => observer.config.refetchOnWindowFocus)
288-
)
287+
this.observers.some(
288+
observer =>
289+
observer.config.enabled && observer.config.refetchOnWindowFocus
290+
)
291+
) {
292+
this.fetch()
293+
}
294+
this.continue()
295+
}
296+
297+
onOnline(): void {
298+
if (
299+
this.state.isStale &&
300+
this.observers.some(
301+
observer =>
302+
observer.config.enabled && observer.config.refetchOnReconnect
303+
)
304+
) {
305+
this.fetch()
306+
}
307+
this.continue()
289308
}
290309

291310
subscribe(
@@ -410,8 +429,8 @@ export class Query<TResult, TError> {
410429
// Delay
411430
await sleep(functionalUpdate(retryDelay, failureCount) || 0)
412431

413-
// Pause retry if the document is not visible
414-
if (!isDocumentVisible()) {
432+
// Pause retry if the document is not visible or when the device is offline
433+
if (!isDocumentVisible() || !isOnline()) {
415434
await new Promise(continueResolve => {
416435
continueLoop = continueResolve
417436
})

src/core/queryCache.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import {
33
deepIncludes,
44
functionalUpdate,
55
getQueryArgs,
6+
isDocumentVisible,
67
isObject,
8+
isOnline,
79
isServer,
810
} from './utils'
911
import { getDefaultedQueryConfig } from './config'
@@ -344,3 +346,17 @@ export const queryCaches = [defaultQueryCache]
344346
export function makeQueryCache(config?: QueryCacheConfig) {
345347
return new QueryCache(config)
346348
}
349+
350+
export function onVisibilityOrOnlineChange(isOnlineChange: boolean) {
351+
if (isDocumentVisible() && isOnline()) {
352+
queryCaches.forEach(queryCache => {
353+
queryCache.getQueries(query => {
354+
if (isOnlineChange) {
355+
query.onOnline()
356+
} else {
357+
query.onWindowFocus()
358+
}
359+
})
360+
})
361+
}
362+
}

src/core/setFocusHandler.ts

Lines changed: 14 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,22 @@
1-
import { isOnline, isDocumentVisible, Console, isServer } from './utils'
2-
import { queryCaches } from './queryCache'
1+
import { createSetHandler, isServer } from './utils'
2+
import { onVisibilityOrOnlineChange } from './queryCache'
33

4-
type FocusHandler = () => void
4+
export const setFocusHandler = createSetHandler(() =>
5+
onVisibilityOrOnlineChange(false)
6+
)
57

6-
const visibilityChangeEvent = 'visibilitychange'
7-
const focusEvent = 'focus'
8-
9-
const onWindowFocus: FocusHandler = () => {
10-
if (isDocumentVisible() && isOnline()) {
11-
queryCaches.forEach(queryCache => {
12-
// Continue any paused queries
13-
queryCache.getQueries(query => {
14-
query.continue()
15-
})
16-
17-
// Invalidate queries which should refetch on window focus
18-
queryCache
19-
.invalidateQueries(query => query.shouldRefetchOnWindowFocus())
20-
.catch(Console.error)
21-
})
22-
}
23-
}
24-
25-
let removePreviousHandler: (() => void) | void
26-
27-
export function setFocusHandler(callback: (callback: FocusHandler) => void) {
28-
// Unsub the old watcher
29-
if (removePreviousHandler) {
30-
removePreviousHandler()
8+
setFocusHandler(handleFocus => {
9+
if (isServer || !window?.addEventListener) {
10+
return
3111
}
32-
// Sub the new watcher
33-
removePreviousHandler = callback(onWindowFocus)
34-
}
3512

36-
setFocusHandler((handleFocus: FocusHandler) => {
3713
// Listen to visibillitychange and focus
38-
if (!isServer && window?.addEventListener) {
39-
window.addEventListener(visibilityChangeEvent, handleFocus, false)
40-
window.addEventListener(focusEvent, handleFocus, false)
14+
window.addEventListener('visibilitychange', handleFocus, false)
15+
window.addEventListener('focus', handleFocus, false)
4116

42-
return () => {
43-
// Be sure to unsubscribe if a new handler is set
44-
window.removeEventListener(visibilityChangeEvent, handleFocus)
45-
window.removeEventListener(focusEvent, handleFocus)
46-
}
17+
return () => {
18+
// Be sure to unsubscribe if a new handler is set
19+
window.removeEventListener('visibilitychange', handleFocus)
20+
window.removeEventListener('focus', handleFocus)
4721
}
48-
return
4922
})

src/core/setOnlineHandler.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { createSetHandler, isServer } from './utils'
2+
import { onVisibilityOrOnlineChange } from './queryCache'
3+
4+
export const setOnlineHandler = createSetHandler(() =>
5+
onVisibilityOrOnlineChange(true)
6+
)
7+
8+
setOnlineHandler(handleOnline => {
9+
if (isServer || !window?.addEventListener) {
10+
return
11+
}
12+
13+
// Listen to online
14+
window.addEventListener('online', handleOnline, false)
15+
16+
return () => {
17+
// Be sure to unsubscribe if a new handler is set
18+
window.removeEventListener('online', handleOnline)
19+
}
20+
})

src/core/tests/queryCache.test.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
queryKey,
44
mockVisibilityState,
55
mockConsoleError,
6+
mockNavigatorOnLine,
67
} from '../../react/tests/utils'
78
import { makeQueryCache, queryCache as defaultQueryCache } from '..'
89
import { isCancelledError, isError } from '../utils'
@@ -435,6 +436,54 @@ describe('queryCache', () => {
435436
expect(result).toBe('data3')
436437
})
437438

439+
it('should continue retry after reconnect and resolve all promises', async () => {
440+
const key = queryKey()
441+
442+
mockNavigatorOnLine(false)
443+
444+
let count = 0
445+
let result
446+
447+
const promise = defaultQueryCache.prefetchQuery(
448+
key,
449+
async () => {
450+
count++
451+
452+
if (count === 3) {
453+
return `data${count}`
454+
}
455+
456+
throw new Error(`error${count}`)
457+
},
458+
{
459+
retry: 3,
460+
retryDelay: 1,
461+
}
462+
)
463+
464+
promise.then(data => {
465+
result = data
466+
})
467+
468+
// Check if we do not have a result
469+
expect(result).toBeUndefined()
470+
471+
// Check if the query is really paused
472+
await sleep(50)
473+
expect(result).toBeUndefined()
474+
475+
// Reset navigator to original value
476+
mockNavigatorOnLine(true)
477+
window.dispatchEvent(new Event('online'))
478+
479+
// There should not be a result yet
480+
expect(result).toBeUndefined()
481+
482+
// By now we should have a value
483+
await sleep(50)
484+
expect(result).toBe('data3')
485+
})
486+
438487
it('should throw a CancelledError when a paused query is cancelled', async () => {
439488
const key = queryKey()
440489

0 commit comments

Comments
 (0)