Skip to content

Commit 324e69c

Browse files
authored
fix(focusManager): stop listening for focus events (#4805)
The `focus` event had many caveats (as discussed in #4797), so we now listen only for `visibilitychange` event.
1 parent dadf96f commit 324e69c

File tree

8 files changed

+41
-77
lines changed

8 files changed

+41
-77
lines changed

docs/react/guides/important-defaults.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ Out of the box, TanStack Query is configured with **aggressive but sane** defaul
1515
- The network is reconnected
1616
- The query is optionally configured with a refetch interval
1717

18-
If you see a refetch that you are not expecting, it is likely because you just focused the window and TanStack Query is doing a [`refetchOnWindowFocus`](../guides/window-focus-refetching). During development, this will probably be triggered more frequently, especially because focusing between the Browser DevTools and your app will also cause a fetch, so be aware of that.
19-
2018
> To change this functionality, you can use options like `refetchOnMount`, `refetchOnWindowFocus`, `refetchOnReconnect` and `refetchInterval`.
2119
2220
- Query results that have no more active instances of `useQuery`, `useInfiniteQuery` or query observers are labeled as "inactive" and remain in the cache in case they are used again at a later time.

docs/react/guides/window-focus-refetching.md

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -48,36 +48,19 @@ In rare circumstances, you may want to manage your own window focus events that
4848

4949
```tsx
5050
focusManager.setEventListener((handleFocus) => {
51-
// Listen to visibilitychange and focus
51+
// Listen to visibilitychange
5252
if (typeof window !== 'undefined' && window.addEventListener) {
5353
window.addEventListener('visibilitychange', handleFocus, false)
54-
window.addEventListener('focus', handleFocus, false)
5554
}
5655

5756
return () => {
5857
// Be sure to unsubscribe if a new handler is set
5958
window.removeEventListener('visibilitychange', handleFocus)
60-
window.removeEventListener('focus', handleFocus)
6159
}
6260
})
6361
```
6462

6563
[//]: # 'Example3'
66-
67-
## Ignoring Iframe Focus Events
68-
69-
A great use-case for replacing the focus handler is that of iframe events. Iframes present problems with detecting window focus by both double-firing events and also firing false-positive events when focusing or using iframes within your app. If you experience this, you should use an event handler that ignores these events as much as possible. I recommend [this one](https://gist.github.com/tannerlinsley/1d3a2122332107fcd8c9cc379be10d88)! It can be set up in the following way:
70-
71-
[//]: # 'Example4'
72-
73-
```tsx
74-
import { focusManager } from '@tanstack/react-query'
75-
import onWindowFocus from './onWindowFocus' // The gist above
76-
77-
focusManager.setEventListener(onWindowFocus) // Boom!
78-
```
79-
80-
[//]: # 'Example4'
8164
[//]: # 'ReactNative'
8265

8366
## Managing Focus in React Native
@@ -105,7 +88,7 @@ useEffect(() => {
10588

10689
## Managing focus state
10790

108-
[//]: # 'Example5'
91+
[//]: # 'Example4'
10992

11093
```tsx
11194
import { focusManager } from '@tanstack/react-query'
@@ -117,8 +100,4 @@ focusManager.setFocused(true)
117100
focusManager.setFocused(undefined)
118101
```
119102

120-
[//]: # 'Example5'
121-
122-
## Pitfalls & Caveats
123-
124-
Some browser internal dialogue windows, such as spawned by `alert()` or file upload dialogues (as created by `<input type="file" />`) might also trigger focus refetching after they close. This can result in unwanted side effects, as the refetching might trigger component unmounts or remounts before your file upload handler is executed. See [this issue on GitHub](https://github.com/tannerlinsley/react-query/issues/2960) for background and possible workarounds.
103+
[//]: # 'Example4'

docs/react/reference/focusManager.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,15 @@ Its available methods are:
2020
```tsx
2121
import { focusManager } from '@tanstack/react-query'
2222

23-
focusManager.setEventListener(handleFocus => {
24-
// Listen to visibilitychange and focus
23+
focusManager.setEventListener((handleFocus) => {
24+
// Listen to visibilitychange
2525
if (typeof window !== 'undefined' && window.addEventListener) {
2626
window.addEventListener('visibilitychange', handleFocus, false)
27-
window.addEventListener('focus', handleFocus, false)
2827
}
2928

3029
return () => {
3130
// Be sure to unsubscribe if a new handler is set
3231
window.removeEventListener('visibilitychange', handleFocus)
33-
window.removeEventListener('focus', handleFocus)
3432
}
3533
})
3634
```

packages/query-core/src/focusManager.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,12 @@ export class FocusManager extends Subscribable {
1818
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1919
if (!isServer && window.addEventListener) {
2020
const listener = () => onFocus()
21-
// Listen to visibillitychange and focus
21+
// Listen to visibilitychange
2222
window.addEventListener('visibilitychange', listener, false)
23-
window.addEventListener('focus', listener, false)
2423

2524
return () => {
2625
// Be sure to unsubscribe if a new handler is set
2726
window.removeEventListener('visibilitychange', listener)
28-
window.removeEventListener('focus', listener)
2927
}
3028
}
3129
return

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

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -87,31 +87,23 @@ describe('focusManager', () => {
8787
})
8888

8989
it('should replace default window listener when a new event listener is set', async () => {
90-
const addEventListenerSpy = jest.spyOn(
91-
globalThis.window,
92-
'addEventListener',
93-
)
90+
const unsubscribeSpy = jest.fn().mockImplementation(() => undefined)
91+
const handlerSpy = jest.fn().mockImplementation(() => unsubscribeSpy)
9492

95-
const removeEventListenerSpy = jest.spyOn(
96-
globalThis.window,
97-
'removeEventListener',
98-
)
93+
focusManager.setEventListener(() => handlerSpy())
9994

100-
// Should set the default event listener with window event listeners
10195
const unsubscribe = focusManager.subscribe(() => undefined)
102-
expect(addEventListenerSpy).toHaveBeenCalledTimes(2)
103-
104-
// Should replace the window default event listener by a new one
105-
// and it should call window.removeEventListener twice
106-
focusManager.setEventListener(() => {
107-
return () => void 0
108-
})
10996

110-
expect(removeEventListenerSpy).toHaveBeenCalledTimes(2)
97+
// Should call the custom event once
98+
expect(handlerSpy).toHaveBeenCalledTimes(1)
11199

112100
unsubscribe()
113-
addEventListenerSpy.mockRestore()
114-
removeEventListenerSpy.mockRestore()
101+
102+
// Should unsubscribe our event event
103+
expect(unsubscribeSpy).toHaveBeenCalledTimes(1)
104+
105+
handlerSpy.mockRestore()
106+
unsubscribeSpy.mockRestore()
115107
})
116108

117109
test('should call removeEventListener when last listener unsubscribes', () => {
@@ -127,12 +119,12 @@ describe('focusManager', () => {
127119

128120
const unsubscribe1 = focusManager.subscribe(() => undefined)
129121
const unsubscribe2 = focusManager.subscribe(() => undefined)
130-
expect(addEventListenerSpy).toHaveBeenCalledTimes(2) // visibilitychange + focus
122+
expect(addEventListenerSpy).toHaveBeenCalledTimes(1) // visibilitychange event
131123

132124
unsubscribe1()
133125
expect(removeEventListenerSpy).toHaveBeenCalledTimes(0)
134126
unsubscribe2()
135-
expect(removeEventListenerSpy).toHaveBeenCalledTimes(2) // visibilitychange + focus
127+
expect(removeEventListenerSpy).toHaveBeenCalledTimes(1) // visibilitychange event
136128
})
137129

138130
test('should keep setup function even if last listener unsubscribes', () => {

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ describe('query', () => {
8686

8787
// Reset visibilityState to original value
8888
visibilityMock.mockRestore()
89-
window.dispatchEvent(new FocusEvent('focus'))
89+
window.dispatchEvent(new Event('visibilitychange'))
9090

9191
// There should not be a result yet
9292
expect(result).toBeUndefined()
@@ -181,7 +181,6 @@ describe('query', () => {
181181
} finally {
182182
// Reset visibilityState to original value
183183
visibilityMock.mockRestore()
184-
window.dispatchEvent(new FocusEvent('focus'))
185184
}
186185
})
187186

packages/react-query/src/__tests__/useQuery.test.tsx

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2608,7 +2608,7 @@ describe('useQuery', () => {
26082608
await waitFor(() => rendered.getByText('default'))
26092609

26102610
act(() => {
2611-
window.dispatchEvent(new FocusEvent('focus'))
2611+
window.dispatchEvent(new Event('visibilitychange'))
26122612
})
26132613

26142614
expect(queryFn).not.toHaveBeenCalled()
@@ -2635,7 +2635,7 @@ describe('useQuery', () => {
26352635
await sleep(10)
26362636

26372637
act(() => {
2638-
window.dispatchEvent(new FocusEvent('focus'))
2638+
window.dispatchEvent(new Event('visibilitychange'))
26392639
})
26402640

26412641
await sleep(10)
@@ -2666,7 +2666,7 @@ describe('useQuery', () => {
26662666
await sleep(10)
26672667

26682668
act(() => {
2669-
window.dispatchEvent(new FocusEvent('focus'))
2669+
window.dispatchEvent(new Event('visibilitychange'))
26702670
})
26712671

26722672
await sleep(10)
@@ -2697,7 +2697,7 @@ describe('useQuery', () => {
26972697
await sleep(10)
26982698

26992699
act(() => {
2700-
window.dispatchEvent(new FocusEvent('focus'))
2700+
window.dispatchEvent(new Event('visibilitychange'))
27012701
})
27022702

27032703
await sleep(10)
@@ -2732,7 +2732,7 @@ describe('useQuery', () => {
27322732
await sleep(20)
27332733

27342734
act(() => {
2735-
window.dispatchEvent(new FocusEvent('focus'))
2735+
window.dispatchEvent(new Event('visibilitychange'))
27362736
})
27372737

27382738
await sleep(20)
@@ -2774,7 +2774,7 @@ describe('useQuery', () => {
27742774
expect(states[1]).toMatchObject({ data: 0, isFetching: false })
27752775

27762776
act(() => {
2777-
window.dispatchEvent(new FocusEvent('focus'))
2777+
window.dispatchEvent(new Event('visibilitychange'))
27782778
})
27792779

27802780
await rendered.findByText('data: 1')
@@ -2786,7 +2786,7 @@ describe('useQuery', () => {
27862786
expect(states[3]).toMatchObject({ data: 1, isFetching: false })
27872787

27882788
act(() => {
2789-
window.dispatchEvent(new FocusEvent('focus'))
2789+
window.dispatchEvent(new Event('visibilitychange'))
27902790
})
27912791

27922792
await sleep(20)
@@ -3519,7 +3519,7 @@ describe('useQuery', () => {
35193519
act(() => {
35203520
// reset visibilityState to original value
35213521
visibilityMock.mockRestore()
3522-
window.dispatchEvent(new FocusEvent('focus'))
3522+
window.dispatchEvent(new Event('visibilitychange'))
35233523
})
35243524

35253525
// Wait for the final result
@@ -3601,7 +3601,7 @@ describe('useQuery', () => {
36013601
act(() => {
36023602
// reset visibilityState to original value
36033603
visibilityMock.mockRestore()
3604-
window.dispatchEvent(new FocusEvent('focus'))
3604+
window.dispatchEvent(new Event('visibilitychange'))
36053605
})
36063606

36073607
await waitFor(() => expect(states.length).toBe(4))
@@ -5327,7 +5327,7 @@ describe('useQuery', () => {
53275327
rendered.getByText('status: success, fetchStatus: paused'),
53285328
)
53295329

5330-
window.dispatchEvent(new FocusEvent('focus'))
5330+
window.dispatchEvent(new Event('visibilitychange'))
53315331
await sleep(15)
53325332

53335333
await waitFor(() =>
@@ -5493,7 +5493,7 @@ describe('useQuery', () => {
54935493

54945494
// triggers a second pause
54955495
act(() => {
5496-
window.dispatchEvent(new FocusEvent('focus'))
5496+
window.dispatchEvent(new Event('visibilitychange'))
54975497
})
54985498

54995499
onlineMock.mockReturnValue(true)

packages/solid-query/src/__tests__/createQuery.test.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2474,7 +2474,7 @@ describe('createQuery', () => {
24742474

24752475
await waitFor(() => screen.getByText('default'))
24762476

2477-
window.dispatchEvent(new FocusEvent('focus'))
2477+
window.dispatchEvent(new Event('visibilitychange'))
24782478

24792479
expect(queryFn).not.toHaveBeenCalled()
24802480
})
@@ -2505,7 +2505,7 @@ describe('createQuery', () => {
25052505

25062506
await sleep(10)
25072507

2508-
window.dispatchEvent(new FocusEvent('focus'))
2508+
window.dispatchEvent(new Event('visibilitychange'))
25092509

25102510
await sleep(10)
25112511

@@ -2540,7 +2540,7 @@ describe('createQuery', () => {
25402540

25412541
await sleep(10)
25422542

2543-
window.dispatchEvent(new FocusEvent('focus'))
2543+
window.dispatchEvent(new Event('visibilitychange'))
25442544

25452545
await sleep(10)
25462546

@@ -2575,7 +2575,7 @@ describe('createQuery', () => {
25752575

25762576
await sleep(10)
25772577

2578-
window.dispatchEvent(new FocusEvent('focus'))
2578+
window.dispatchEvent(new Event('visibilitychange'))
25792579

25802580
await sleep(10)
25812581

@@ -2613,7 +2613,7 @@ describe('createQuery', () => {
26132613

26142614
await sleep(20)
26152615

2616-
window.dispatchEvent(new FocusEvent('focus'))
2616+
window.dispatchEvent(new Event('visibilitychange'))
26172617

26182618
await sleep(20)
26192619

@@ -2658,7 +2658,7 @@ describe('createQuery', () => {
26582658
expect(states[0]).toMatchObject({ data: undefined, isFetching: true })
26592659
expect(states[1]).toMatchObject({ data: 0, isFetching: false })
26602660

2661-
window.dispatchEvent(new FocusEvent('focus'))
2661+
window.dispatchEvent(new Event('visibilitychange'))
26622662

26632663
await screen.findByText('data: 1')
26642664

@@ -3469,7 +3469,7 @@ describe('createQuery', () => {
34693469
await waitFor(() => screen.getByText('failureReason fetching error 1'))
34703470

34713471
visibilityMock.mockRestore()
3472-
window.dispatchEvent(new FocusEvent('focus'))
3472+
window.dispatchEvent(new Event('visibilitychange'))
34733473

34743474
// Wait for the final result
34753475
await waitFor(() => screen.getByText('failureCount 4'))
@@ -3564,7 +3564,7 @@ describe('createQuery', () => {
35643564

35653565
// reset visibilityState to original value
35663566
visibilityMock.mockRestore()
3567-
window.dispatchEvent(new FocusEvent('focus'))
3567+
window.dispatchEvent(new Event('visibilitychange'))
35683568

35693569
await waitFor(() => expect(states.length).toBe(4))
35703570

@@ -5367,7 +5367,7 @@ describe('createQuery', () => {
53675367
screen.getByText('status: success, fetchStatus: paused'),
53685368
)
53695369

5370-
window.dispatchEvent(new FocusEvent('focus'))
5370+
window.dispatchEvent(new Event('visibilitychange'))
53715371
await sleep(15)
53725372

53735373
await waitFor(() =>
@@ -5544,7 +5544,7 @@ describe('createQuery', () => {
55445544
)
55455545

55465546
// triggers a second pause
5547-
window.dispatchEvent(new FocusEvent('focus'))
5547+
window.dispatchEvent(new Event('visibilitychange'))
55485548

55495549
onlineMock.mockReturnValue(true)
55505550
window.dispatchEvent(new Event('online'))

0 commit comments

Comments
 (0)