Skip to content

fix: Reusing cache provider #1539

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 23 additions & 13 deletions src/use-swr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,19 +79,23 @@ export const useSWRHandler = <Data = any, Error = any>(
const data = isUndefined(cached) ? fallback : cached
const error = cache.get(keyErr)

// A revalidation must be triggered when mounted if:
// - `revalidateOnMount` is explicitly set to `true`.
// - `isPaused()` returns `false`, and:
// - Suspense mode and there's stale data for the initial render.
// - Not suspense mode and there is no fallback data and `revalidateIfStale` is enabled.
// - `revalidateIfStale` is enabled but `data` is not defined.
const shouldRevalidateOnMount = () => {
// If `revalidateOnMount` is set, we take the value directly.
if (!isUndefined(revalidateOnMount)) return revalidateOnMount

// If it's paused, we skip revalidation.
if (getConfig().isPaused()) return false

return suspense
? !initialMountedRef.current && !isUndefined(data)
: isUndefined(data) || config.revalidateIfStale
? // Under suspense mode, we will have to revalidate if there's no stale
// data to return.
!isUndefined(data)
: // If there is no stale data, we need to revalidate on mount;
// If `revalidateIfStale` is set to true, we will always revalidate.
isUndefined(data) || config.revalidateIfStale
}

// Resolve the current validating state.
Expand Down Expand Up @@ -153,7 +157,10 @@ export const useSWRHandler = <Data = any, Error = any>(
const newState: State<Data, Error> = { isValidating: false }
const finishRequestAndUpdateState = () => {
cache.set(keyValidating, false)
setState(newState)
// We can only set state if it's safe (still mounted with the same key).
if (isCallbackSafe()) {
setState(newState)
}
}

// Start fetching. Change the `isValidating` state, update the cache.
Expand All @@ -175,8 +182,9 @@ export const useSWRHandler = <Data = any, Error = any>(
// we trigger the loading slow event.
if (config.loadingTimeout && !cache.get(key)) {
setTimeout(() => {
if (loading && isCallbackSafe())
if (loading && isCallbackSafe()) {
getConfig().onLoadingSlow(key, config)
}
}, config.loadingTimeout)
}

Expand Down Expand Up @@ -204,7 +212,9 @@ export const useSWRHandler = <Data = any, Error = any>(
// CONCURRENT_PROMISES_TS[key] maybe be `undefined` or a number
if (CONCURRENT_PROMISES_TS[key] !== startAt) {
if (shouldStartNewRequest) {
getConfig().onDiscarded(key)
if (isCallbackSafe()) {
getConfig().onDiscarded(key)
}
}
return false
}
Expand Down Expand Up @@ -236,7 +246,9 @@ export const useSWRHandler = <Data = any, Error = any>(
) {
finishRequestAndUpdateState()
if (shouldStartNewRequest) {
getConfig().onDiscarded(key)
if (isCallbackSafe()) {
getConfig().onDiscarded(key)
}
}
return false
}
Expand Down Expand Up @@ -292,7 +304,7 @@ export const useSWRHandler = <Data = any, Error = any>(

// Here is the source of the request, need to tell all other hooks to
// update their states.
if (shouldStartNewRequest) {
if (isCallbackSafe() && shouldStartNewRequest) {
broadcastState(cache, key, newState.data, newState.error, false)
}

Expand Down Expand Up @@ -389,6 +401,7 @@ export const useSWRHandler = <Data = any, Error = any>(
// Mark the component as mounted and update corresponding refs.
unmountedRef.current = false
keyRef.current = key
initialMountedRef.current = true

// When `key` updates, reset the state to the initial value
// and trigger a rerender if necessary.
Expand All @@ -412,9 +425,6 @@ export const useSWRHandler = <Data = any, Error = any>(
}
}

// Finally, the component is mounted.
initialMountedRef.current = true

return () => {
// Mark it as unmounted.
unmountedRef.current = true
Expand Down
19 changes: 15 additions & 4 deletions src/utils/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ const revalidateAllKeys = (
export const initCache = <Data = any>(
provider: Cache<Data>,
options?: Partial<ProviderConfiguration>
): [Cache<Data>, ScopedMutator<Data>, () => void] | undefined => {
):
| [Cache<Data>, ScopedMutator<Data>, () => void]
| [Cache<Data>, ScopedMutator<Data>]
| undefined => {
// The global state for a specific provider will be used to deduplicate
// requests and store listeners. As well as a mutate function that bound to
// the cache.
Expand All @@ -41,7 +44,7 @@ export const initCache = <Data = any>(
const mutate = internalMutate.bind(UNDEFINED, provider) as ScopedMutator<
Data
>
let unsubscribe = noop
let unmount = noop

// Update the state if it's new, or the provider has been extended.
SWRGlobalState.set(provider, [
Expand Down Expand Up @@ -71,15 +74,23 @@ export const initCache = <Data = any>(
revalidateEvents.RECONNECT_EVENT
)
)
unsubscribe = () => {
unmount = () => {
releaseFocus && releaseFocus()
releaseReconnect && releaseReconnect()

// When un-mounting, we need to remove the cache provider from the state
// storage too because it's a side-effect. Otherwise when re-mounting we
// will not re-register those event listeners.
// @ts-ignore
SWRGlobalState.delete(provider)
}
}

// We might want to inject an extra layer on top of `provider` in the future,
// such as key serialization, auto GC, etc.
// For now, it's just a `Map` interface without any modifications.
return [provider, mutate, unsubscribe]
return [provider, mutate, unmount]
}

return [provider, SWRGlobalState.get(provider)![6]]
}
68 changes: 67 additions & 1 deletion test/use-swr-cache.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { act, fireEvent, screen } from '@testing-library/react'
import React, { useState } from 'react'
import React, { useState, StrictMode } from 'react'
import useSWR, { useSWRConfig, SWRConfig, mutate as globalMutate } from 'swr'
import {
sleep,
Expand Down Expand Up @@ -413,4 +413,70 @@ describe('useSWR - global cache', () => {
screen.getByText('fallback') // no `undefined`, directly fallback
await screen.findByText('data')
})

it('should reusing the same cache instance after unmounting SWRConfig', async () => {
let focusEventRegistered = false

const cacheSingleton = new Map([['key', 'value']])
function Page() {
return (
<SWRConfig
value={{
provider: () => cacheSingleton,
initFocus: () => {
focusEventRegistered = true
return () => (focusEventRegistered = false)
}
}}
>
<Comp />
</SWRConfig>
)
}
function Comp() {
const { cache } = useSWRConfig()
return <>{String(cache.get('key'))}</>
}

function Wrapper() {
const [mount, setMountPage] = useState(true)
return (
<>
<button onClick={() => setMountPage(!mount)}>toggle</button>
{mount ? <Page /> : null}
</>
)
}

renderWithGlobalCache(<Wrapper />)
await screen.findByText('value')
fireEvent.click(screen.getByText('toggle'))
fireEvent.click(screen.getByText('toggle'))
await screen.findByText('value')

expect(focusEventRegistered).toEqual(true)
})

it('should correctly return the cache instance under strict mode', async () => {
function Page() {
// Intentionally do this.
const [cache] = useState(new Map([['key', 'value']]))
return (
<SWRConfig value={{ provider: () => cache }}>
<Comp />
</SWRConfig>
)
}
function Comp() {
const { cache } = useSWRConfig()
return <>{String(cache.get('key'))}</>
}

renderWithGlobalCache(
<StrictMode>
<Page />
</StrictMode>
)
await screen.findByText('value')
})
})