Skip to content

Commit

Permalink
perf: avoid unnecessary re-renders with the suspense mode
Browse files Browse the repository at this point in the history
  • Loading branch information
koba04 committed Feb 15, 2021
1 parent 883ff9e commit 0771899
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 68 deletions.
125 changes: 66 additions & 59 deletions src/use-swr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ function useSWR<Data = any, Error = any>(
}
}

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
Expand Down Expand Up @@ -722,57 +722,16 @@ function useSWR<Data = any, Error = any>(
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
Expand Down Expand Up @@ -809,21 +768,69 @@ function useSWR<Data = any, Error = any>(
// 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
}

Expand Down
16 changes: 7 additions & 9 deletions test/use-swr-suspense.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <div>{data}</div>
}
Expand All @@ -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
})
})

0 comments on commit 0771899

Please sign in to comment.