From 0771899bc7cc509d746dc142f4bdd0cb9195e041 Mon Sep 17 00:00:00 2001 From: Toru Kobayashi Date: Tue, 16 Feb 2021 01:18:40 +0900 Subject: [PATCH] perf: avoid unnecessary re-renders with the suspense mode --- src/use-swr.ts | 125 +++++++++++++++++---------------- test/use-swr-suspense.test.tsx | 16 ++--- 2 files changed, 73 insertions(+), 68 deletions(-) diff --git a/src/use-swr.ts b/src/use-swr.ts index c09eee2c7..324192921 100644 --- a/src/use-swr.ts +++ b/src/use-swr.ts @@ -318,7 +318,7 @@ function useSWR( } } - if (shouldUpdateState || config.suspense) { + if (shouldUpdateState) { // if component is unmounted, should skip rerender // if component is not mounted, should skip rerender if (unmountedRef.current || !initialMountedRef.current) return @@ -722,57 +722,16 @@ function useSWR( revalidate ]) - // define returned state - // can be memorized since the state is a ref - const memoizedState = useMemo(() => { - const state = { revalidate, mutate: boundMutate } as responseInterface< - Data, - Error - > - Object.defineProperties(state, { - error: { - // `key` might be changed in the upcoming hook re-render, - // but the previous state will stay - // so we need to match the latest key and data (fallback to `initialData`) - get: function() { - stateDependencies.current.error = true - return keyRef.current === key ? stateRef.current.error : initialError - }, - enumerable: true - }, - data: { - get: function() { - stateDependencies.current.data = true - return keyRef.current === key ? stateRef.current.data : initialData - }, - enumerable: true - }, - isValidating: { - get: function() { - stateDependencies.current.isValidating = true - return key ? stateRef.current.isValidating : false - }, - enumerable: true - } - }) - - return state - // `boundMutate` is immutable, and the immutability of `revalidate` depends on `key` - // so we can omit them from the deps array, - // but we put it to enable react-hooks/exhaustive-deps rule. - // `initialData` and `initialError` are not initial values - // because they are changed during the lifecycle - // so we should add them in the deps array. - }, [revalidate, initialData, initialError, boundMutate, key]) - // suspense + let latestData + let latestError if (config.suspense) { // in suspense mode, we can't return empty state // (it should be suspended) // try to get data and error from cache - let latestData = cache.get(key) - let latestError = cache.get(keyErr) + latestData = cache.get(key) + latestError = cache.get(keyErr) if (typeof latestData === 'undefined') { latestData = initialData @@ -809,21 +768,69 @@ function useSWR( // in suspense mode, throw error if there's no content throw latestError } - - // return the latest data / error from cache - // in case `key` has changed - return { - error: latestError, - data: latestData, - // revalidate will be deprecated in the 1.x release - // because mutate() covers the same use case of revalidate(). - // This remains only for backward compatibility - revalidate, - mutate: boundMutate, - isValidating: stateRef.current.isValidating - } } + // define returned state + // can be memorized since the state is a ref + const memoizedState = useMemo(() => { + // revalidate will be deprecated in the 1.x release + // because mutate() covers the same use case of revalidate(). + // This remains only for backward compatibility + const state = { revalidate, mutate: boundMutate } as responseInterface< + Data, + Error + > + Object.defineProperties(state, { + error: { + // `key` might be changed in the upcoming hook re-render, + // but the previous state will stay + // so we need to match the latest key and data (fallback to `initialData`) + get: function() { + stateDependencies.current.error = true + if (config.suspense) { + return latestError + } + return keyRef.current === key ? stateRef.current.error : initialError + }, + enumerable: true + }, + data: { + get: function() { + stateDependencies.current.data = true + if (config.suspense) { + return latestData + } + return keyRef.current === key ? stateRef.current.data : initialData + }, + enumerable: true + }, + isValidating: { + get: function() { + stateDependencies.current.isValidating = true + return key ? stateRef.current.isValidating : false + }, + enumerable: true + } + }) + + return state + // `config.suspense` isn't allowed to change during the lifecycle. + // `boundMutate` is immutable, and the immutability of `revalidate` depends on `key` + // so we can omit them from the deps array, + // but we put it to enable react-hooks/exhaustive-deps rule. + // `initialData` and `initialError` are not initial values + // because they are changed during the lifecycle + // so we should add them in the deps array. + }, [ + revalidate, + initialData, + initialError, + boundMutate, + key, + config.suspense, + latestError, + latestData + ]) return memoizedState } diff --git a/test/use-swr-suspense.test.tsx b/test/use-swr-suspense.test.tsx index 156ee2545..e70b20be6 100644 --- a/test/use-swr-suspense.test.tsx +++ b/test/use-swr-suspense.test.tsx @@ -214,19 +214,18 @@ describe('useSWR - suspense', () => { `"hello, Initial"` ) }) - it.only('should not avoid unnecessary renders', async () => { + it('should avoid unnecessary re-renders', async () => { let renderCount = 0 - let beforeRenderCount = 0 + let startRenderCount = 0 function Section() { - ++beforeRenderCount - const { data, isValidating } = useSWR( + ++startRenderCount + const { data } = useSWR( 'suspense-10', () => new Promise(res => setTimeout(() => res('SWR'), 100)), { suspense: true } ) - console.log(data, isValidating) ++renderCount return
{data}
} @@ -238,10 +237,9 @@ describe('useSWR - suspense', () => { // hydration expect(container.textContent).toMatchInlineSnapshot(`"fallback"`) - await act(() => sleep(110)) // update await screen.findByText('SWR') - await act(() => sleep(2000)) // update - expect(beforeRenderCount).toBe(4) - expect(renderCount).toBe(3) + await act(() => sleep(100)) // wait a moment to observe unnecessary renders + expect(startRenderCount).toBe(2) // fallback + data + expect(renderCount).toBe(1) // data }) })