From f49f8f6733dc290c92085faa59062c93d0f08737 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Sat, 16 Apr 2022 18:15:37 +0200 Subject: [PATCH 1/4] add laggy option --- src/types.ts | 1 + src/use-swr.ts | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/types.ts b/src/types.ts index 3a30349c5..4b808de79 100644 --- a/src/types.ts +++ b/src/types.ts @@ -41,6 +41,7 @@ export interface PublicConfiguration< revalidateIfStale: boolean shouldRetryOnError: boolean | ((err: Error) => boolean) suspense?: boolean + laggy?: boolean fallbackData?: Data fetcher?: Fn use?: Middleware[] diff --git a/src/use-swr.ts b/src/use-swr.ts index bd6fe8fea..c67e01b54 100644 --- a/src/use-swr.ts +++ b/src/use-swr.ts @@ -54,9 +54,10 @@ export const useSWRHandler = ( ) => { const { cache, + laggy, compare, - fallbackData, suspense, + fallbackData, revalidateOnMount, refreshInterval, refreshWhenHidden, @@ -98,7 +99,11 @@ export const useSWRHandler = ( 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 = laggy ? laggyDataRef.current : 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. @@ -387,11 +392,17 @@ export const useSWRHandler = ( [] ) - // 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. @@ -518,7 +529,7 @@ export const useSWRHandler = ( }, [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. @@ -536,7 +547,7 @@ export const useSWRHandler = ( mutate: boundMutate, get data() { stateDependencies.data = true - return data + return returnedData }, get error() { stateDependencies.error = true From 0559bf32de57bc284471c2c4c1fbb9da16ea7194 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Sat, 16 Apr 2022 18:20:47 +0200 Subject: [PATCH 2/4] use keepPreviousData --- src/types.ts | 2 +- src/use-swr.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/types.ts b/src/types.ts index 4b808de79..0dea512cd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -40,8 +40,8 @@ export interface PublicConfiguration< revalidateOnMount?: boolean revalidateIfStale: boolean shouldRetryOnError: boolean | ((err: Error) => boolean) + keepPreviousData?: boolean suspense?: boolean - laggy?: boolean fallbackData?: Data fetcher?: Fn use?: Middleware[] diff --git a/src/use-swr.ts b/src/use-swr.ts index c67e01b54..6eb95d497 100644 --- a/src/use-swr.ts +++ b/src/use-swr.ts @@ -54,14 +54,14 @@ export const useSWRHandler = ( ) => { const { cache, - laggy, compare, suspense, fallbackData, revalidateOnMount, refreshInterval, refreshWhenHidden, - refreshWhenOffline + refreshWhenOffline, + keepPreviousData } = config const [EVENT_REVALIDATORS, STATE_UPDATERS, MUTATION, FETCH] = @@ -103,7 +103,7 @@ export const useSWRHandler = ( const laggyDataRef = useRef(data) const isInitialMount = !initialMountedRef.current - const returnedData = laggy ? laggyDataRef.current : data + const returnedData = keepPreviousData ? laggyDataRef.current : 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. From 6959c392727dbfc86af35aa61b55def72153041e Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Sat, 16 Apr 2022 20:01:40 +0200 Subject: [PATCH 3/4] add test cases --- src/use-swr.ts | 10 ++- test/use-swr-laggy.test.tsx | 163 ++++++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 test/use-swr-laggy.test.tsx diff --git a/src/use-swr.ts b/src/use-swr.ts index 6eb95d497..0e78a6c1c 100644 --- a/src/use-swr.ts +++ b/src/use-swr.ts @@ -103,7 +103,11 @@ export const useSWRHandler = ( const laggyDataRef = useRef(data) const isInitialMount = !initialMountedRef.current - const returnedData = keepPreviousData ? laggyDataRef.current : data + 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. @@ -184,8 +188,10 @@ export const useSWRHandler = ( 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) } diff --git a/test/use-swr-laggy.test.tsx b/test/use-swr-laggy.test.tsx new file mode 100644 index 000000000..9df4ea279 --- /dev/null +++ b/test/use-swr-laggy.test.tsx @@ -0,0 +1,163 @@ +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 + } + + renderWithConfig() + 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 + } + + renderWithConfig() + 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 + } + + renderWithConfig() + 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 ( + <> + + + + ) + } + + renderWithConfig() + 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 + } + + renderWithConfig() + 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]] + ]) + }) +}) From 972cefc9b8228c1be84b0976ed6c3e53e439b973 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Sat, 16 Apr 2022 20:07:29 +0200 Subject: [PATCH 4/4] add new test --- test/use-swr-laggy.test.tsx | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/use-swr-laggy.test.tsx b/test/use-swr-laggy.test.tsx index 9df4ea279..0f770c1df 100644 --- a/test/use-swr-laggy.test.tsx +++ b/test/use-swr-laggy.test.tsx @@ -160,4 +160,38 @@ describe('useSWR - keep previous data', () => { [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 + } + + renderWithConfig() + 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] + ]) + }) })