Skip to content

Commit 195afe3

Browse files
authored
fix(onlineManager): always initialize with online: true (#5714)
* fix(onlineManager): always initialize with `online: true` instead of relying on navigator.onLine, because that indicator is broken AF in chrome https://bugs.chromium.org/p/chromium/issues/list?q=navigator.online * docs: document subscribe methods * docs * test: fix types in tests setting to undefined is now illegal, so we have to reset to `true`, which is the default now * fix(tests): switch from mocking navigator.onLine to mocking onlineManager.isOnline * fix: offline toggle in devtools it should now be enough to set online to true / false, without firing the event, because we always set & respect that value. we don't override the event handler, so real online / offline events might interfere here * chore: fix tests with the implementation of onlineManager, where we default to `true`, we need an explicit `'offline'` event to get it to false; otherwise, switching back to true doesn't change the state, so subscribers are not informed * chore: prettier write * chore: fix eslint * chore: delete an old, flaky test that doesn't test much
1 parent 56d1620 commit 195afe3

File tree

16 files changed

+182
-251
lines changed

16 files changed

+182
-251
lines changed

docs/react/guides/migrating-to-v5.md

+8
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,14 @@ There are some caveats to this change however, which you must be aware of:
276276

277277
The `visibilitychange` event is used exclusively now. This is possible because we only support browsers that support the `visibilitychange` event. This fixes a bunch of issues [as listed here](https://github.com/TanStack/query/pull/4805).
278278

279+
### Network status no longer relies on the `navigator.onLine` property
280+
281+
`navigator.onLine` doesn't work well in Chromium based browsers. There are [a lot of issues](https://bugs.chromium.org/p/chromium/issues/list?q=navigator.online) around false negatives, which lead to Queries being wrongfully marked as `offline`.
282+
283+
To circumvent this, we now always start with `online: true` and only listen to `online` and `offline` events to update the status.
284+
285+
This should reduce the likelihood of false negatives, however, it might mean false positives for offline apps that load via serviceWorkers, which can work even without an internet connection.
286+
279287
### Removed custom `context` prop in favor of custom `queryClient` instance
280288

281289
In v4, we introduced the possibility to pass a custom `context` to all react-query hooks. This allowed for proper isolation when using MicroFrontends.

docs/react/reference/focusManager.md

+14-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ It can be used to change the default event listeners or to manually change the f
1010
Its available methods are:
1111

1212
- [`setEventListener`](#focusmanagerseteventlistener)
13+
- [`subscribe`](#focusmanagersubscribe)
1314
- [`setFocused`](#focusmanagersetfocused)
1415
- [`isFocused`](#focusmanagerisfocused)
1516

@@ -33,9 +34,21 @@ focusManager.setEventListener((handleFocus) => {
3334
})
3435
```
3536

37+
## `focusManager.subscribe`
38+
39+
`subscribe` can be used to subscribe to changes in the visibility state. It returns an unsubscribe function:
40+
41+
```tsx
42+
import { focusManager } from '@tanstack/react-query'
43+
44+
const unsubscribe = focusManager.subscribe(isVisible => {
45+
console.log('isVisible', isVisible)
46+
})
47+
```
48+
3649
## `focusManager.setFocused`
3750

38-
`setFocused` can be used to manually set the focus state. Set `undefined` to fallback to the default focus check.
51+
`setFocused` can be used to manually set the focus state. Set `undefined` to fall back to the default focus check.
3952

4053
```tsx
4154
import { focusManager } from '@tanstack/react-query'

docs/react/reference/onlineManager.md

+23-7
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,20 @@ id: OnlineManager
33
title: OnlineManager
44
---
55

6-
The `OnlineManager` manages the online state within TanStack Query.
6+
The `OnlineManager` manages the online state within TanStack Query. It can be used to change the default event listeners or to manually change the online state.
77

8-
It can be used to change the default event listeners or to manually change the online state.
8+
> Per default, the `onlineManager` assumes an active network connection, and listens to the `online` and `offline` events on the `window` object to detect changes.
9+
10+
> In previous versions, `navigator.onLine` was used to determine the network status. However, it doesn't work well in Chromium based browsers. There are [a lot of issues](https://bugs.chromium.org/p/chromium/issues/list?q=navigator.online) around false negatives, which lead to Queries being wrongfully marked as `offline`.
11+
12+
> To circumvent this, we now always start with `online: true` and only listen to `online` and `offline` events to update the status.
13+
14+
> This should reduce the likelihood of false negatives, however, it might mean false positives for offline apps that load via serviceWorkers, which can work even without an internet connection.
915
1016
Its available methods are:
1117

1218
- [`setEventListener`](#onlinemanagerseteventlistener)
19+
- [`subscribe`](#onlinemanagersubscribe)
1320
- [`setOnline`](#onlinemanagersetonline)
1421
- [`isOnline`](#onlinemanagerisonline)
1522

@@ -28,9 +35,21 @@ onlineManager.setEventListener(setOnline => {
2835
})
2936
```
3037

38+
## `onlineManager.subscribe`
39+
40+
`subscribe` can be used to subscribe to changes in the online state. It returns an unsubscribe function:
41+
42+
```tsx
43+
import { onlineManager } from '@tanstack/react-query'
44+
45+
const unsubscribe = onlineManager.subscribe(isOnline => {
46+
console.log('isOnline', isOnline)
47+
})
48+
```
49+
3150
## `onlineManager.setOnline`
3251

33-
`setOnline` can be used to manually set the online state. Set `undefined` to fallback to the default online check.
52+
`setOnline` can be used to manually set the online state.
3453

3554
```tsx
3655
import { onlineManager } from '@tanstack/react-query'
@@ -40,14 +59,11 @@ onlineManager.setOnline(true)
4059

4160
// Set to offline
4261
onlineManager.setOnline(false)
43-
44-
// Fallback to the default online check
45-
onlineManager.setOnline(undefined)
4662
```
4763

4864
**Options**
4965

50-
- `online: boolean | undefined`
66+
- `online: boolean`
5167

5268
## `onlineManager.isOnline`
5369

packages/query-core/src/onlineManager.ts

+16-41
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
import { Subscribable } from './subscribable'
22
import { isServer } from './utils'
33

4-
type SetupFn = (
5-
setOnline: (online?: boolean) => void,
6-
) => (() => void) | undefined
4+
type Listener = (online: boolean) => void
5+
type SetupFn = (setOnline: Listener) => (() => void) | undefined
76

8-
const onlineEvents = ['online', 'offline'] as const
9-
10-
export class OnlineManager extends Subscribable {
11-
#online?: boolean
7+
export class OnlineManager extends Subscribable<Listener> {
8+
#online = true
129
#cleanup?: () => void
1310

1411
#setup: SetupFn
@@ -19,17 +16,16 @@ export class OnlineManager extends Subscribable {
1916
// addEventListener does not exist in React Native, but window does
2017
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
2118
if (!isServer && window.addEventListener) {
22-
const listener = () => onOnline()
19+
const onlineListener = () => onOnline(true)
20+
const offlineListener = () => onOnline(false)
2321
// Listen to online
24-
onlineEvents.forEach((event) => {
25-
window.addEventListener(event, listener, false)
26-
})
22+
window.addEventListener('online', onlineListener, false)
23+
window.addEventListener('offline', offlineListener, false)
2724

2825
return () => {
2926
// Be sure to unsubscribe if a new handler is set
30-
onlineEvents.forEach((event) => {
31-
window.removeEventListener(event, listener)
32-
})
27+
window.removeEventListener('online', onlineListener)
28+
window.removeEventListener('offline', offlineListener)
3329
}
3430
}
3531

@@ -53,43 +49,22 @@ export class OnlineManager extends Subscribable {
5349
setEventListener(setup: SetupFn): void {
5450
this.#setup = setup
5551
this.#cleanup?.()
56-
this.#cleanup = setup((online?: boolean) => {
57-
if (typeof online === 'boolean') {
58-
this.setOnline(online)
59-
} else {
60-
this.onOnline()
61-
}
62-
})
52+
this.#cleanup = setup(this.setOnline.bind(this))
6353
}
6454

65-
setOnline(online?: boolean): void {
55+
setOnline(online: boolean): void {
6656
const changed = this.#online !== online
6757

6858
if (changed) {
6959
this.#online = online
70-
this.onOnline()
60+
this.listeners.forEach((listener) => {
61+
listener(online)
62+
})
7163
}
7264
}
7365

74-
onOnline(): void {
75-
this.listeners.forEach((listener) => {
76-
listener()
77-
})
78-
}
79-
8066
isOnline(): boolean {
81-
if (typeof this.#online === 'boolean') {
82-
return this.#online
83-
}
84-
85-
if (
86-
typeof navigator === 'undefined' ||
87-
typeof navigator.onLine === 'undefined'
88-
) {
89-
return true
90-
}
91-
92-
return navigator.onLine
67+
return this.#online
9368
}
9469
}
9570

packages/query-core/src/tests/hydration.test.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { MutationCache } from '../mutationCache'
55
import {
66
createQueryClient,
77
executeMutation,
8-
mockNavigatorOnLine,
8+
mockOnlineManagerIsOnline,
99
sleep,
1010
} from './utils'
1111

@@ -347,7 +347,7 @@ describe('dehydration and rehydration', () => {
347347
test('should be able to dehydrate mutations and continue on hydration', async () => {
348348
const consoleMock = vi.spyOn(console, 'error')
349349
consoleMock.mockImplementation(() => undefined)
350-
const onlineMock = mockNavigatorOnLine(false)
350+
const onlineMock = mockOnlineManagerIsOnline(false)
351351

352352
const serverAddTodo = vi
353353
.fn()

packages/query-core/src/tests/onlineManager.test.tsx

+6-10
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ describe('onlineManager', () => {
3131
test('setEventListener should use online boolean arg', async () => {
3232
let count = 0
3333

34-
const setup = (setOnline: (online?: boolean) => void) => {
34+
const setup = (setOnline: (online: boolean) => void) => {
3535
setTimeout(() => {
3636
count++
3737
setOnline(false)
@@ -154,19 +154,15 @@ describe('onlineManager', () => {
154154

155155
onlineManager.subscribe(listener)
156156

157-
onlineManager.setOnline(true)
158-
onlineManager.setOnline(true)
159-
160-
expect(listener).toHaveBeenCalledTimes(1)
161-
162157
onlineManager.setOnline(false)
163158
onlineManager.setOnline(false)
164159

165-
expect(listener).toHaveBeenCalledTimes(2)
160+
expect(listener).toHaveBeenNthCalledWith(1, false)
166161

167-
onlineManager.setOnline(undefined)
168-
onlineManager.setOnline(undefined)
162+
onlineManager.setOnline(true)
163+
onlineManager.setOnline(true)
169164

170-
expect(listener).toHaveBeenCalledTimes(3)
165+
expect(listener).toHaveBeenCalledTimes(2)
166+
expect(listener).toHaveBeenNthCalledWith(2, true)
171167
})
172168
})

packages/query-core/src/tests/queryClient.test.tsx

+9-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { waitFor } from '@testing-library/react'
22
import '@testing-library/jest-dom'
33

4-
import { vi } from 'vitest'
54
import {
65
MutationObserver,
76
QueryObserver,
@@ -11,7 +10,7 @@ import {
1110
import { noop } from '../utils'
1211
import {
1312
createQueryClient,
14-
mockNavigatorOnLine,
13+
mockOnlineManagerIsOnline,
1514
queryKey,
1615
sleep,
1716
} from './utils'
@@ -1074,7 +1073,7 @@ describe('queryClient', () => {
10741073
const key1 = queryKey()
10751074
const queryFn1 = vi.fn<unknown[], string>().mockReturnValue('data1')
10761075
await queryClient.fetchQuery({ queryKey: key1, queryFn: queryFn1 })
1077-
const onlineMock = mockNavigatorOnLine(false)
1076+
const onlineMock = mockOnlineManagerIsOnline(false)
10781077

10791078
await queryClient.refetchQueries({ queryKey: key1 })
10801079

@@ -1088,7 +1087,7 @@ describe('queryClient', () => {
10881087
queryClient.setQueryDefaults(key1, { networkMode: 'always' })
10891088
const queryFn1 = vi.fn<unknown[], string>().mockReturnValue('data1')
10901089
await queryClient.fetchQuery({ queryKey: key1, queryFn: queryFn1 })
1091-
const onlineMock = mockNavigatorOnLine(false)
1090+
const onlineMock = mockOnlineManagerIsOnline(false)
10921091

10931092
await queryClient.refetchQueries({ queryKey: key1 })
10941093

@@ -1394,7 +1393,7 @@ describe('queryClient', () => {
13941393
queryCacheOnFocusSpy.mockRestore()
13951394
queryCacheOnOnlineSpy.mockRestore()
13961395
mutationCacheResumePausedMutationsSpy.mockRestore()
1397-
onlineManager.setOnline(undefined)
1396+
onlineManager.setOnline(true)
13981397
})
13991398

14001399
test('should resume paused mutations when coming online', async () => {
@@ -1424,7 +1423,7 @@ describe('queryClient', () => {
14241423
expect(observer1.getCurrentResult().status).toBe('success')
14251424
})
14261425

1427-
onlineManager.setOnline(undefined)
1426+
onlineManager.setOnline(true)
14281427
})
14291428

14301429
test('should resume paused mutations one after the other when invoked manually at the same time', async () => {
@@ -1459,7 +1458,7 @@ describe('queryClient', () => {
14591458
expect(observer2.getCurrentResult().isPaused).toBeTruthy()
14601459
})
14611460

1462-
onlineManager.setOnline(undefined)
1461+
onlineManager.setOnline(true)
14631462
void queryClient.resumePausedMutations()
14641463
await sleep(5)
14651464
await queryClient.resumePausedMutations()
@@ -1491,6 +1490,7 @@ describe('queryClient', () => {
14911490
'resumePausedMutations',
14921491
)
14931492

1493+
onlineManager.setOnline(false)
14941494
onlineManager.setOnline(true)
14951495
expect(queryCacheOnOnlineSpy).toHaveBeenCalledTimes(1)
14961496
expect(mutationCacheResumePausedMutationsSpy).toHaveBeenCalledTimes(1)
@@ -1503,7 +1503,7 @@ describe('queryClient', () => {
15031503
queryCacheOnOnlineSpy.mockRestore()
15041504
mutationCacheResumePausedMutationsSpy.mockRestore()
15051505
focusManager.setFocused(undefined)
1506-
onlineManager.setOnline(undefined)
1506+
onlineManager.setOnline(true)
15071507
})
15081508

15091509
test('should not notify queryCache and mutationCache after multiple mounts/unmounts', async () => {
@@ -1538,7 +1538,7 @@ describe('queryClient', () => {
15381538
queryCacheOnOnlineSpy.mockRestore()
15391539
mutationCacheResumePausedMutationsSpy.mockRestore()
15401540
focusManager.setFocused(undefined)
1541-
onlineManager.setOnline(undefined)
1541+
onlineManager.setOnline(true)
15421542
})
15431543
})
15441544

packages/query-core/src/tests/utils.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { act } from '@testing-library/react'
22
import { vi } from 'vitest'
3-
import { QueryClient } from '..'
3+
import { QueryClient, onlineManager } from '..'
44
import * as utils from '../utils'
55
import type { SpyInstance } from 'vitest'
66
import type { MutationOptions, QueryClientConfig } from '..'
@@ -15,8 +15,10 @@ export function mockVisibilityState(
1515
return vi.spyOn(document, 'visibilityState', 'get').mockReturnValue(value)
1616
}
1717

18-
export function mockNavigatorOnLine(value: boolean): SpyInstance<[], boolean> {
19-
return vi.spyOn(navigator, 'onLine', 'get').mockReturnValue(value)
18+
export function mockOnlineManagerIsOnline(
19+
value: boolean,
20+
): SpyInstance<[], boolean> {
21+
return vi.spyOn(onlineManager, 'isOnline').mockReturnValue(value)
2022
}
2123

2224
let queryKeyCount = 0

packages/query-devtools/src/Devtools.tsx

+1-3
Original file line numberDiff line numberDiff line change
@@ -485,13 +485,11 @@ export const DevtoolsPanel: Component<DevtoolsPanelProps> = (props) => {
485485
<button
486486
onClick={() => {
487487
if (offline()) {
488-
onlineManager().setOnline(undefined)
488+
onlineManager().setOnline(true)
489489
setOffline(false)
490-
window.dispatchEvent(new Event('online'))
491490
} else {
492491
onlineManager().setOnline(false)
493492
setOffline(true)
494-
window.dispatchEvent(new Event('offline'))
495493
}
496494
}}
497495
class={styles.actionsBtn}

0 commit comments

Comments
 (0)