Skip to content

Commit 1df45b7

Browse files
shudingnevilm-lt
authored andcommitted
Skip error retrying when document is not active and improve tests (vercel#1742)
* use the babel coverage reporter * improve coverage
1 parent b6c6dda commit 1df45b7

9 files changed

+137
-20
lines changed

jest.config.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,5 @@ module.exports = {
1919
]
2020
},
2121
coveragePathIgnorePatterns: ['/node_modules/', '/dist/', '/test/'],
22-
coverageProvider: 'v8',
23-
coverageReporters: ['text']
22+
coverageReporters: ['text', 'html']
2423
}

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export { useSWRConfig } from './utils/use-swr-config'
88
export { mutate } from './utils/config'
99

1010
// Types
11-
export {
11+
export type {
1212
SWRConfiguration,
1313
Revalidator,
1414
RevalidatorOptions,

src/use-swr.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export const useSWRHandler = <Data = any, Error = any>(
7676
const fetcherRef = useRef(fetcher)
7777
const configRef = useRef(config)
7878
const getConfig = () => configRef.current
79+
const isActive = () => getConfig().isVisible() && getConfig().isOnline()
7980

8081
// Get the current state that SWR should return.
8182
const cached = cache.get(key)
@@ -305,10 +306,14 @@ export const useSWRHandler = <Data = any, Error = any>(
305306
getConfig().onError(err, key, config)
306307
if (config.shouldRetryOnError) {
307308
// When retrying, dedupe is always enabled
308-
getConfig().onErrorRetry(err, key, config, revalidate, {
309-
retryCount: (opts.retryCount || 0) + 1,
310-
dedupe: true
311-
})
309+
if (isActive()) {
310+
// If it's active, stop. It will auto revalidate when refocusing
311+
// or reconnecting.
312+
getConfig().onErrorRetry(err, key, config, revalidate, {
313+
retryCount: (opts.retryCount || 0) + 1,
314+
dedupe: true
315+
})
316+
}
312317
}
313318
}
314319
}
@@ -366,8 +371,6 @@ export const useSWRHandler = <Data = any, Error = any>(
366371
const keyChanged = initialMountedRef.current
367372
const softRevalidate = revalidate.bind(UNDEFINED, WITH_DEDUPE)
368373

369-
const isActive = () => getConfig().isVisible() && getConfig().isOnline()
370-
371374
// Expose state updater to global event listeners. So we can update hook's
372375
// internal state from the outside.
373376
const onStateUpdate: StateUpdateCallback<Data, Error> = (

src/utils/config.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,6 @@ const onErrorRetry = (
2020
revalidate: Revalidator,
2121
opts: Required<RevalidatorOptions>
2222
): void => {
23-
if (!preset.isVisible()) {
24-
// If it's hidden, stop. It will auto revalidate when refocusing.
25-
return
26-
}
27-
2823
const maxRetryCount = config.errorRetryCount
2924
const currentRetryCount = opts.retryCount
3025

src/utils/web-preset.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,7 @@ const offDocumentEvent = hasDoc
3030

3131
const isVisible = () => {
3232
const visibilityState = hasDoc && document.visibilityState
33-
if (!isUndefined(visibilityState)) {
34-
return visibilityState !== 'hidden'
35-
}
36-
return true
33+
return isUndefined(visibilityState) || visibilityState !== 'hidden'
3734
}
3835

3936
const initFocus = (callback: () => void) => {

test/unit/serialize.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { unstable_serialize } from 'swr'
2+
import { stableHash } from '../../src/utils/hash'
3+
4+
describe('SWR - unstable_serialize', () => {
5+
it('should serialize arguments correctly', async () => {
6+
expect(unstable_serialize([])).toBe('')
7+
expect(unstable_serialize(null)).toBe('')
8+
expect(unstable_serialize('key')).toBe('key')
9+
expect(unstable_serialize([1, { foo: 2, bar: 1 }, ['a', 'b', 'c']])).toBe(
10+
stableHash([1, { foo: 2, bar: 1 }, ['a', 'b', 'c']])
11+
)
12+
})
13+
})

test/use-swr-error.test.tsx

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { act, fireEvent, screen } from '@testing-library/react'
22
import React, { useEffect, useState } from 'react'
33
import useSWR from 'swr'
4-
import { sleep, createResponse, createKey, renderWithConfig } from './utils'
4+
import {
5+
sleep,
6+
createResponse,
7+
createKey,
8+
renderWithConfig,
9+
mockVisibilityHidden
10+
} from './utils'
511

612
describe('useSWR - error', () => {
713
it('should handle errors', async () => {
@@ -72,6 +78,70 @@ describe('useSWR - error', () => {
7278
screen.getByText('error: 2')
7379
})
7480

81+
it('should stop retrying when document is not visible', async () => {
82+
const key = createKey()
83+
let count = 0
84+
function Page() {
85+
const { data, error } = useSWR(
86+
key,
87+
() => createResponse(new Error('error: ' + count++), { delay: 100 }),
88+
{
89+
onErrorRetry: (_, __, ___, revalidate, revalidateOpts) => {
90+
revalidate(revalidateOpts)
91+
},
92+
dedupingInterval: 0
93+
}
94+
)
95+
if (error) return <div>{error.message}</div>
96+
return <div>hello, {data}</div>
97+
}
98+
renderWithConfig(<Page />)
99+
screen.getByText('hello,')
100+
101+
// mount
102+
await screen.findByText('error: 0')
103+
104+
// errored, retrying
105+
await act(() => sleep(50))
106+
const resetVisibility = mockVisibilityHidden()
107+
108+
await act(() => sleep(100))
109+
screen.getByText('error: 1')
110+
111+
await act(() => sleep(100)) // stopped due to invisible
112+
screen.getByText('error: 1')
113+
114+
resetVisibility()
115+
})
116+
117+
it('should not retry when shouldRetryOnError is disabled', async () => {
118+
const key = createKey()
119+
let count = 0
120+
function Page() {
121+
const { data, error } = useSWR(
122+
key,
123+
() => createResponse(new Error('error: ' + count++), { delay: 100 }),
124+
{
125+
onErrorRetry: (_, __, ___, revalidate, revalidateOpts) => {
126+
revalidate(revalidateOpts)
127+
},
128+
dedupingInterval: 0,
129+
shouldRetryOnError: false
130+
}
131+
)
132+
if (error) return <div>{error.message}</div>
133+
return <div>hello, {data}</div>
134+
}
135+
renderWithConfig(<Page />)
136+
screen.getByText('hello,')
137+
138+
// mount
139+
await screen.findByText('error: 0')
140+
141+
await act(() => sleep(150))
142+
screen.getByText('error: 0')
143+
})
144+
75145
it('should trigger the onLoadingSlow and onSuccess event', async () => {
76146
const key = createKey()
77147
let loadingSlow = null,

test/use-swr-reconnect.test.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import useSWR from 'swr'
44
import {
55
nextTick as waitForNextTick,
66
renderWithConfig,
7-
createKey
7+
createKey,
8+
mockVisibilityHidden
89
} from './utils'
910

1011
describe('useSWR - reconnect', () => {
@@ -90,4 +91,37 @@ describe('useSWR - reconnect', () => {
9091
// should not be revalidated
9192
screen.getByText('data: 0')
9293
})
94+
95+
it("shouldn't revalidate on reconnect if invisible", async () => {
96+
let value = 0
97+
98+
const key = createKey()
99+
function Page() {
100+
const { data } = useSWR(key, () => value++, {
101+
dedupingInterval: 0,
102+
isOnline: () => false
103+
})
104+
return <div>data: {data}</div>
105+
}
106+
107+
renderWithConfig(<Page />)
108+
// hydration
109+
screen.getByText('data:')
110+
111+
// mount
112+
await screen.findByText('data: 0')
113+
114+
await waitForNextTick()
115+
116+
const resetVisibility = mockVisibilityHidden()
117+
118+
// trigger reconnect
119+
fireEvent(window, createEvent('offline', window))
120+
fireEvent(window, createEvent('online', window))
121+
122+
// should not be revalidated
123+
screen.getByText('data: 0')
124+
125+
resetVisibility()
126+
})
93127
})

test/utils.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,9 @@ export const renderWithGlobalCache = (
5353
): ReturnType<typeof _renderWithConfig> => {
5454
return _renderWithConfig(element, { ...config })
5555
}
56+
57+
export const mockVisibilityHidden = () => {
58+
const mockVisibilityState = jest.spyOn(document, 'visibilityState', 'get')
59+
mockVisibilityState.mockImplementation(() => 'hidden')
60+
return () => mockVisibilityState.mockRestore()
61+
}

0 commit comments

Comments
 (0)