Skip to content

Commit

Permalink
Add keepPreviousData option (#1929)
Browse files Browse the repository at this point in the history
* add laggy option

* use keepPreviousData

* add test cases

* add new test
  • Loading branch information
shuding committed Apr 16, 2022
1 parent 0f552d8 commit 3e71e9a
Show file tree
Hide file tree
Showing 3 changed files with 221 additions and 6 deletions.
1 change: 1 addition & 0 deletions src/types.ts
Expand Up @@ -40,6 +40,7 @@ export interface PublicConfiguration<
revalidateOnMount?: boolean
revalidateIfStale: boolean
shouldRetryOnError: boolean | ((err: Error) => boolean)
keepPreviousData?: boolean
suspense?: boolean
fallbackData?: Data
fetcher?: Fn
Expand Down
29 changes: 23 additions & 6 deletions src/use-swr.ts
Expand Up @@ -55,12 +55,13 @@ export const useSWRHandler = <Data = any, Error = any>(
const {
cache,
compare,
fallbackData,
suspense,
fallbackData,
revalidateOnMount,
refreshInterval,
refreshWhenHidden,
refreshWhenOffline
refreshWhenOffline,
keepPreviousData
} = config

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

// Use a ref to store previous returned data. Use the inital data as its inital value.
const laggyDataRef = useRef(data)

const isInitialMount = !initialMountedRef.current
const returnedData = keepPreviousData
? isUndefined(cachedData)
? laggyDataRef.current
: cachedData
: data

// - Suspense mode and there's stale data for the initial render.
// - Not suspense mode and there is no fallback data and `revalidateIfStale` is enabled.
Expand Down Expand Up @@ -179,8 +188,10 @@ export const useSWRHandler = <Data = any, Error = any>(
isLoading: false
}
const finishRequestAndUpdateState = () => {
// Set the global cache.
setCache(finalState)
// We can only set state if it's safe (still mounted with the same key).

// We can only set the local state if it's safe (still mounted with the same key).
if (isCurrentKeyMounted()) {
setState(finalState)
}
Expand Down Expand Up @@ -387,11 +398,17 @@ export const useSWRHandler = <Data = any, Error = any>(
[]
)

// Always update fetcher, config and state refs.
// Logic for updating refs.
useIsomorphicLayoutEffect(() => {
fetcherRef.current = fetcher
configRef.current = config
stateRef.current = currentState

// Handle laggy data updates. If there's cached data of the current key,
// it'll be the correct reference.
if (!isUndefined(cachedData)) {
laggyDataRef.current = cachedData
}
})

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

// Display debug info in React DevTools.
useDebugValue(data)
useDebugValue(returnedData)

// In Suspense mode, we can't return the empty `data` state.
// If there is `error`, the `error` needs to be thrown to the error boundary.
Expand All @@ -536,7 +553,7 @@ export const useSWRHandler = <Data = any, Error = any>(
mutate: boundMutate,
get data() {
stateDependencies.data = true
return data
return returnedData
},
get error() {
stateDependencies.error = true
Expand Down
197 changes: 197 additions & 0 deletions test/use-swr-laggy.test.tsx
@@ -0,0 +1,197 @@
import { screen, act, fireEvent } from '@testing-library/react'
import React, { useState } from 'react'
import useSWR from 'swr'
import useSWRInfinite from 'swr/infinite'

import { createKey, createResponse, renderWithConfig, sleep } from './utils'

describe('useSWR - keep previous data', () => {
it('should keep previous data when key changes when `keepPreviousData` is enabled', async () => {
const loggedData = []
const fetcher = k => createResponse(k, { delay: 50 })
function App() {
const [key, setKey] = useState(createKey())
const { data: laggedData } = useSWR(key, fetcher, {
keepPreviousData: true
})
loggedData.push([key, laggedData])
return <button onClick={() => setKey(createKey())}>change key</button>
}

renderWithConfig(<App />)
await act(() => sleep(100))
fireEvent.click(screen.getByText('change key'))
await act(() => sleep(100))

const key1 = loggedData[0][0]
const key2 = loggedData[2][0]
expect(loggedData).toEqual([
[key1, undefined],
[key1, key1],
[key2, key1],
[key2, key2]
])
})

it('should keep previous data when sharing the cache', async () => {
const loggedData = []
const fetcher = k => createResponse(k, { delay: 50 })
function App() {
const [key, setKey] = useState(createKey())

const { data } = useSWR(key, fetcher)
const { data: laggedData } = useSWR(key, fetcher, {
keepPreviousData: true
})

loggedData.push([key, data, laggedData])
return <button onClick={() => setKey(createKey())}>change key</button>
}

renderWithConfig(<App />)
await act(() => sleep(100))
fireEvent.click(screen.getByText('change key'))
await act(() => sleep(100))

const key1 = loggedData[0][0]
const key2 = loggedData[2][0]
expect(loggedData).toEqual([
[key1, undefined, undefined],
[key1, key1, key1],
[key2, undefined, key1],
[key2, key2, key2]
])
})

it('should keep previous data even if there is fallback data', async () => {
const loggedData = []
const fetcher = k => createResponse(k, { delay: 50 })
function App() {
const [key, setKey] = useState(createKey())

const { data } = useSWR(key, fetcher, {
fallbackData: 'fallback'
})
const { data: laggedData } = useSWR(key, fetcher, {
keepPreviousData: true,
fallbackData: 'fallback'
})

loggedData.push([key, data, laggedData])
return <button onClick={() => setKey(createKey())}>change key</button>
}

renderWithConfig(<App />)
await act(() => sleep(100))
fireEvent.click(screen.getByText('change key'))
await act(() => sleep(100))

const key1 = loggedData[0][0]
const key2 = loggedData[2][0]
expect(loggedData).toEqual([
[key1, 'fallback', 'fallback'],
[key1, key1, key1],
[key2, 'fallback', key1],
[key2, key2, key2]
])
})

it('should always return the latest data', async () => {
const loggedData = []
const fetcher = k => createResponse(k, { delay: 50 })
function App() {
const [key, setKey] = useState(createKey())
const { data: laggedData, mutate } = useSWR(key, fetcher, {
keepPreviousData: true
})
loggedData.push([key, laggedData])
return (
<>
<button onClick={() => setKey(createKey())}>change key</button>
<button onClick={() => mutate('mutate')}>mutate</button>
</>
)
}

renderWithConfig(<App />)
await act(() => sleep(100))
fireEvent.click(screen.getByText('change key'))
await act(() => sleep(100))
fireEvent.click(screen.getByText('mutate'))
await act(() => sleep(100))

const key1 = loggedData[0][0]
const key2 = loggedData[2][0]
expect(loggedData).toEqual([
[key1, undefined],
[key1, key1],
[key2, key1],
[key2, key2],
[key2, 'mutate'],
[key2, key2]
])
})

it('should keep previous data for the useSWRInfinite hook', async () => {
const loggedData = []
const fetcher = k => createResponse(k, { delay: 50 })
function App() {
const [key, setKey] = useState(createKey())

const { data } = useSWRInfinite(() => key, fetcher, {
keepPreviousData: true
})

loggedData.push([key, data])
return <button onClick={() => setKey(createKey())}>change key</button>
}

renderWithConfig(<App />)
await act(() => sleep(100))
fireEvent.click(screen.getByText('change key'))
await act(() => sleep(100))

const key1 = loggedData[0][0]
const key2 = loggedData[2][0]
expect(loggedData).toEqual([
[key1, undefined],
[key1, [key1]],
[key2, [key1]],
[key2, [key2]]
])
})

it('should support changing the `keepPreviousData` option', async () => {
const loggedData = []
const fetcher = k => createResponse(k, { delay: 50 })
let keepPreviousData = false
function App() {
const [key, setKey] = useState(createKey())
const { data: laggedData } = useSWR(key, fetcher, {
keepPreviousData
})
loggedData.push([key, laggedData])
return <button onClick={() => setKey(createKey())}>change key</button>
}

renderWithConfig(<App />)
await act(() => sleep(100))
fireEvent.click(screen.getByText('change key'))
await act(() => sleep(100))
keepPreviousData = true
fireEvent.click(screen.getByText('change key'))
await act(() => sleep(100))

const key1 = loggedData[0][0]
const key2 = loggedData[2][0]
const key3 = loggedData[4][0]
expect(loggedData).toEqual([
[key1, undefined],
[key1, key1],
[key2, undefined],
[key2, key2],
[key3, key2],
[key3, key3]
])
})
})

0 comments on commit 3e71e9a

Please sign in to comment.