Skip to content

Commit dd690b1

Browse files
shudinghimanshiLt
authored andcommitted
Add keepPreviousData option (vercel#1929)
* add laggy option * use keepPreviousData * add test cases * add new test
1 parent 390fe5e commit dd690b1

File tree

3 files changed

+221
-6
lines changed

3 files changed

+221
-6
lines changed

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export interface PublicConfiguration<
4040
revalidateOnMount?: boolean
4141
revalidateIfStale: boolean
4242
shouldRetryOnError: boolean | ((err: Error) => boolean)
43+
keepPreviousData?: boolean
4344
suspense?: boolean
4445
fallbackData?: Data
4546
fetcher?: Fn

src/use-swr.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,13 @@ export const useSWRHandler = <Data = any, Error = any>(
5555
const {
5656
cache,
5757
compare,
58-
fallbackData,
5958
suspense,
59+
fallbackData,
6060
revalidateOnMount,
6161
refreshInterval,
6262
refreshWhenHidden,
63-
refreshWhenOffline
63+
refreshWhenOffline,
64+
keepPreviousData
6465
} = config
6566

6667
const [EVENT_REVALIDATORS, STATE_UPDATERS, MUTATION, FETCH] =
@@ -98,7 +99,15 @@ export const useSWRHandler = <Data = any, Error = any>(
9899
const data = isUndefined(cachedData) ? fallback : cachedData
99100
const error = cached.error
100101

102+
// Use a ref to store previous returned data. Use the inital data as its inital value.
103+
const laggyDataRef = useRef(data)
104+
101105
const isInitialMount = !initialMountedRef.current
106+
const returnedData = keepPreviousData
107+
? isUndefined(cachedData)
108+
? laggyDataRef.current
109+
: cachedData
110+
: data
102111

103112
// - Suspense mode and there's stale data for the initial render.
104113
// - Not suspense mode and there is no fallback data and `revalidateIfStale` is enabled.
@@ -179,8 +188,10 @@ export const useSWRHandler = <Data = any, Error = any>(
179188
isLoading: false
180189
}
181190
const finishRequestAndUpdateState = () => {
191+
// Set the global cache.
182192
setCache(finalState)
183-
// We can only set state if it's safe (still mounted with the same key).
193+
194+
// We can only set the local state if it's safe (still mounted with the same key).
184195
if (isCurrentKeyMounted()) {
185196
setState(finalState)
186197
}
@@ -387,11 +398,17 @@ export const useSWRHandler = <Data = any, Error = any>(
387398
[]
388399
)
389400

390-
// Always update fetcher, config and state refs.
401+
// Logic for updating refs.
391402
useIsomorphicLayoutEffect(() => {
392403
fetcherRef.current = fetcher
393404
configRef.current = config
394405
stateRef.current = currentState
406+
407+
// Handle laggy data updates. If there's cached data of the current key,
408+
// it'll be the correct reference.
409+
if (!isUndefined(cachedData)) {
410+
laggyDataRef.current = cachedData
411+
}
395412
})
396413

397414
// After mounted or key changed.
@@ -518,7 +535,7 @@ export const useSWRHandler = <Data = any, Error = any>(
518535
}, [refreshInterval, refreshWhenHidden, refreshWhenOffline, key])
519536

520537
// Display debug info in React DevTools.
521-
useDebugValue(data)
538+
useDebugValue(returnedData)
522539

523540
// In Suspense mode, we can't return the empty `data` state.
524541
// If there is `error`, the `error` needs to be thrown to the error boundary.
@@ -536,7 +553,7 @@ export const useSWRHandler = <Data = any, Error = any>(
536553
mutate: boundMutate,
537554
get data() {
538555
stateDependencies.data = true
539-
return data
556+
return returnedData
540557
},
541558
get error() {
542559
stateDependencies.error = true

test/use-swr-laggy.test.tsx

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { screen, act, fireEvent } from '@testing-library/react'
2+
import React, { useState } from 'react'
3+
import useSWR from 'swr'
4+
import useSWRInfinite from 'swr/infinite'
5+
6+
import { createKey, createResponse, renderWithConfig, sleep } from './utils'
7+
8+
describe('useSWR - keep previous data', () => {
9+
it('should keep previous data when key changes when `keepPreviousData` is enabled', async () => {
10+
const loggedData = []
11+
const fetcher = k => createResponse(k, { delay: 50 })
12+
function App() {
13+
const [key, setKey] = useState(createKey())
14+
const { data: laggedData } = useSWR(key, fetcher, {
15+
keepPreviousData: true
16+
})
17+
loggedData.push([key, laggedData])
18+
return <button onClick={() => setKey(createKey())}>change key</button>
19+
}
20+
21+
renderWithConfig(<App />)
22+
await act(() => sleep(100))
23+
fireEvent.click(screen.getByText('change key'))
24+
await act(() => sleep(100))
25+
26+
const key1 = loggedData[0][0]
27+
const key2 = loggedData[2][0]
28+
expect(loggedData).toEqual([
29+
[key1, undefined],
30+
[key1, key1],
31+
[key2, key1],
32+
[key2, key2]
33+
])
34+
})
35+
36+
it('should keep previous data when sharing the cache', async () => {
37+
const loggedData = []
38+
const fetcher = k => createResponse(k, { delay: 50 })
39+
function App() {
40+
const [key, setKey] = useState(createKey())
41+
42+
const { data } = useSWR(key, fetcher)
43+
const { data: laggedData } = useSWR(key, fetcher, {
44+
keepPreviousData: true
45+
})
46+
47+
loggedData.push([key, data, laggedData])
48+
return <button onClick={() => setKey(createKey())}>change key</button>
49+
}
50+
51+
renderWithConfig(<App />)
52+
await act(() => sleep(100))
53+
fireEvent.click(screen.getByText('change key'))
54+
await act(() => sleep(100))
55+
56+
const key1 = loggedData[0][0]
57+
const key2 = loggedData[2][0]
58+
expect(loggedData).toEqual([
59+
[key1, undefined, undefined],
60+
[key1, key1, key1],
61+
[key2, undefined, key1],
62+
[key2, key2, key2]
63+
])
64+
})
65+
66+
it('should keep previous data even if there is fallback data', async () => {
67+
const loggedData = []
68+
const fetcher = k => createResponse(k, { delay: 50 })
69+
function App() {
70+
const [key, setKey] = useState(createKey())
71+
72+
const { data } = useSWR(key, fetcher, {
73+
fallbackData: 'fallback'
74+
})
75+
const { data: laggedData } = useSWR(key, fetcher, {
76+
keepPreviousData: true,
77+
fallbackData: 'fallback'
78+
})
79+
80+
loggedData.push([key, data, laggedData])
81+
return <button onClick={() => setKey(createKey())}>change key</button>
82+
}
83+
84+
renderWithConfig(<App />)
85+
await act(() => sleep(100))
86+
fireEvent.click(screen.getByText('change key'))
87+
await act(() => sleep(100))
88+
89+
const key1 = loggedData[0][0]
90+
const key2 = loggedData[2][0]
91+
expect(loggedData).toEqual([
92+
[key1, 'fallback', 'fallback'],
93+
[key1, key1, key1],
94+
[key2, 'fallback', key1],
95+
[key2, key2, key2]
96+
])
97+
})
98+
99+
it('should always return the latest data', async () => {
100+
const loggedData = []
101+
const fetcher = k => createResponse(k, { delay: 50 })
102+
function App() {
103+
const [key, setKey] = useState(createKey())
104+
const { data: laggedData, mutate } = useSWR(key, fetcher, {
105+
keepPreviousData: true
106+
})
107+
loggedData.push([key, laggedData])
108+
return (
109+
<>
110+
<button onClick={() => setKey(createKey())}>change key</button>
111+
<button onClick={() => mutate('mutate')}>mutate</button>
112+
</>
113+
)
114+
}
115+
116+
renderWithConfig(<App />)
117+
await act(() => sleep(100))
118+
fireEvent.click(screen.getByText('change key'))
119+
await act(() => sleep(100))
120+
fireEvent.click(screen.getByText('mutate'))
121+
await act(() => sleep(100))
122+
123+
const key1 = loggedData[0][0]
124+
const key2 = loggedData[2][0]
125+
expect(loggedData).toEqual([
126+
[key1, undefined],
127+
[key1, key1],
128+
[key2, key1],
129+
[key2, key2],
130+
[key2, 'mutate'],
131+
[key2, key2]
132+
])
133+
})
134+
135+
it('should keep previous data for the useSWRInfinite hook', async () => {
136+
const loggedData = []
137+
const fetcher = k => createResponse(k, { delay: 50 })
138+
function App() {
139+
const [key, setKey] = useState(createKey())
140+
141+
const { data } = useSWRInfinite(() => key, fetcher, {
142+
keepPreviousData: true
143+
})
144+
145+
loggedData.push([key, data])
146+
return <button onClick={() => setKey(createKey())}>change key</button>
147+
}
148+
149+
renderWithConfig(<App />)
150+
await act(() => sleep(100))
151+
fireEvent.click(screen.getByText('change key'))
152+
await act(() => sleep(100))
153+
154+
const key1 = loggedData[0][0]
155+
const key2 = loggedData[2][0]
156+
expect(loggedData).toEqual([
157+
[key1, undefined],
158+
[key1, [key1]],
159+
[key2, [key1]],
160+
[key2, [key2]]
161+
])
162+
})
163+
164+
it('should support changing the `keepPreviousData` option', async () => {
165+
const loggedData = []
166+
const fetcher = k => createResponse(k, { delay: 50 })
167+
let keepPreviousData = false
168+
function App() {
169+
const [key, setKey] = useState(createKey())
170+
const { data: laggedData } = useSWR(key, fetcher, {
171+
keepPreviousData
172+
})
173+
loggedData.push([key, laggedData])
174+
return <button onClick={() => setKey(createKey())}>change key</button>
175+
}
176+
177+
renderWithConfig(<App />)
178+
await act(() => sleep(100))
179+
fireEvent.click(screen.getByText('change key'))
180+
await act(() => sleep(100))
181+
keepPreviousData = true
182+
fireEvent.click(screen.getByText('change key'))
183+
await act(() => sleep(100))
184+
185+
const key1 = loggedData[0][0]
186+
const key2 = loggedData[2][0]
187+
const key3 = loggedData[4][0]
188+
expect(loggedData).toEqual([
189+
[key1, undefined],
190+
[key1, key1],
191+
[key2, undefined],
192+
[key2, key2],
193+
[key3, key2],
194+
[key3, key3]
195+
])
196+
})
197+
})

0 commit comments

Comments
 (0)