Skip to content

Commit 91e5836

Browse files
committed
feat: add refetch on reconnect functionality
1 parent 90fb8a4 commit 91e5836

File tree

12 files changed

+157
-53
lines changed

12 files changed

+157
-53
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: 24 additions & 7 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,
@@ -280,12 +281,28 @@ 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+
}
295+
296+
onOnline(): void {
297+
if (
298+
this.state.isStale &&
299+
this.observers.some(
300+
observer =>
301+
observer.config.enabled && observer.config.refetchOnReconnect
302+
)
303+
) {
304+
this.fetch()
305+
}
289306
}
290307

291308
subscribe(
@@ -410,8 +427,8 @@ export class Query<TResult, TError> {
410427
// Delay
411428
await sleep(functionalUpdate(retryDelay, failureCount) || 0)
412429

413-
// Pause retry if the document is not visible
414-
if (!isDocumentVisible()) {
430+
// Pause retry if the document is not visible or when the device is offline
431+
if (!isDocumentVisible() || !isOnline()) {
415432
await new Promise(continueResolve => {
416433
continueLoop = continueResolve
417434
})

src/core/queryCache.ts

Lines changed: 20 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,21 @@ 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+
// Trigger query hooks
355+
if (isOnlineChange) {
356+
query.onOnline()
357+
} else {
358+
query.onWindowFocus()
359+
}
360+
361+
// Continue any paused queries
362+
query.continue()
363+
})
364+
})
365+
}
366+
}

src/core/setFocusHandler.ts

Lines changed: 15 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,29 @@
1-
import { isOnline, isDocumentVisible, Console, isServer } from './utils'
2-
import { queryCaches } from './queryCache'
3-
4-
type FocusHandler = () => void
5-
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-
}
1+
import { isServer } from './utils'
2+
import { onVisibilityOrOnlineChange } from './queryCache'
243

254
let removePreviousHandler: (() => void) | void
265

27-
export function setFocusHandler(callback: (callback: FocusHandler) => void) {
6+
export function setFocusHandler(callback: (handler: () => void) => void) {
287
// Unsub the old watcher
298
if (removePreviousHandler) {
309
removePreviousHandler()
3110
}
3211
// Sub the new watcher
33-
removePreviousHandler = callback(onWindowFocus)
12+
removePreviousHandler = callback(() => onVisibilityOrOnlineChange(false))
3413
}
3514

36-
setFocusHandler((handleFocus: FocusHandler) => {
15+
setFocusHandler(handleFocus => {
16+
if (isServer || !window?.addEventListener) {
17+
return
18+
}
19+
3720
// Listen to visibillitychange and focus
38-
if (!isServer && window?.addEventListener) {
39-
window.addEventListener(visibilityChangeEvent, handleFocus, false)
40-
window.addEventListener(focusEvent, handleFocus, false)
21+
window.addEventListener('visibilitychange', handleFocus, false)
22+
window.addEventListener('focus', handleFocus, false)
4123

42-
return () => {
43-
// Be sure to unsubscribe if a new handler is set
44-
window.removeEventListener(visibilityChangeEvent, handleFocus)
45-
window.removeEventListener(focusEvent, handleFocus)
46-
}
24+
return () => {
25+
// Be sure to unsubscribe if a new handler is set
26+
window.removeEventListener('visibilitychange', handleFocus)
27+
window.removeEventListener('focus', handleFocus)
4728
}
48-
return
4929
})

src/core/setOnlineHandler.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { isServer } from './utils'
2+
import { onVisibilityOrOnlineChange } from './queryCache'
3+
4+
let removePreviousHandler: (() => void) | void
5+
6+
export function setOnlineHandler(callback: (handler: () => void) => void) {
7+
// Unsub the old watcher
8+
if (removePreviousHandler) {
9+
removePreviousHandler()
10+
}
11+
// Sub the new watcher
12+
removePreviousHandler = callback(() => onVisibilityOrOnlineChange(true))
13+
}
14+
15+
setOnlineHandler(handleOnline => {
16+
if (isServer || !window?.addEventListener) {
17+
return
18+
}
19+
20+
// Listen to online
21+
window.addEventListener('online', handleOnline, false)
22+
23+
return () => {
24+
// Be sure to unsubscribe if a new handler is set
25+
window.removeEventListener('online', handleOnline)
26+
}
27+
})

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)