diff --git a/src/types.ts b/src/types.ts index 206a00ae6..115657512 100644 --- a/src/types.ts +++ b/src/types.ts @@ -64,8 +64,8 @@ export interface PublicConfiguration< export type FullConfiguration = InternalConfiguration & PublicConfiguration export type ConfigOptions = { - initFocus: (callback: () => void) => void - initReconnect: (callback: () => void) => void + initFocus: (callback: () => void) => (() => void) | void + initReconnect: (callback: () => void) => (() => void) | void } export type SWRHook = ( diff --git a/src/use-swr.ts b/src/use-swr.ts index 30b799f38..5a6bd77a5 100644 --- a/src/use-swr.ts +++ b/src/use-swr.ts @@ -85,7 +85,7 @@ export const useSWRHandler = ( // A revalidation must be triggered when mounted if: // - `revalidateOnMount` is explicitly set to `true`. - // - `isPaused()` returns 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. diff --git a/src/utils/cache.ts b/src/utils/cache.ts index 953a470fe..d7fb32acd 100644 --- a/src/utils/cache.ts +++ b/src/utils/cache.ts @@ -25,7 +25,7 @@ const revalidateAllKeys = ( export const initCache = ( provider: Cache, options?: Partial -): [Cache, ScopedMutator] | undefined => { +): [Cache, ScopedMutator, () => void] | 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. @@ -55,28 +55,31 @@ export const initCache = ( // This is a new provider, we need to initialize it and setup DOM events // listeners for `focus` and `reconnect` actions. + let unscubscibe = () => {} if (!IS_SERVER) { - opts.initFocus( + const releaseFocus = opts.initFocus( revalidateAllKeys.bind( UNDEFINED, EVENT_REVALIDATORS, revalidateEvents.FOCUS_EVENT ) ) - opts.initReconnect( + const releaseReconnect = opts.initReconnect( revalidateAllKeys.bind( UNDEFINED, EVENT_REVALIDATORS, revalidateEvents.RECONNECT_EVENT ) ) + unscubscibe = () => { + releaseFocus && releaseFocus() + releaseReconnect && releaseReconnect() + } } // 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] + return [provider, mutate, unscubscibe] } - - return } diff --git a/src/utils/config-context.ts b/src/utils/config-context.ts index b7a71f657..4f9254596 100644 --- a/src/utils/config-context.ts +++ b/src/utils/config-context.ts @@ -1,9 +1,15 @@ -import { createContext, createElement, useContext, useState, FC } from 'react' - +import { + createContext, + createElement, + useContext, + useState, + FC, + useEffect +} from 'react' import { cache as defaultCache } from './config' import { initCache } from './cache' import { mergeConfigs } from './merge-config' -import { UNDEFINED } from './helper' +import { isFunction, UNDEFINED } from './helper' import { SWRConfiguration, FullConfiguration, @@ -26,18 +32,25 @@ const SWRConfig: FC<{ const provider = value && value.provider // Use a lazy initialized state to create the cache on first access. - const [cacheAndMutate] = useState(() => + const [cacheHandle] = useState(() => provider ? initCache(provider(extendedConfig.cache || defaultCache), value) : UNDEFINED ) // Override the cache if a new provider is given. - if (cacheAndMutate) { - extendedConfig.cache = cacheAndMutate[0] - extendedConfig.mutate = cacheAndMutate[1] + if (cacheHandle) { + extendedConfig.cache = cacheHandle[0] + extendedConfig.mutate = cacheHandle[1] } + useEffect(() => { + return () => { + const unsubscribe = cacheHandle ? cacheHandle[2] : UNDEFINED + isFunction(unsubscribe) && unsubscribe() + } + }, []) + return createElement( SWRConfigContext.Provider, { value: extendedConfig }, diff --git a/src/utils/config.ts b/src/utils/config.ts index b7cd5c707..f6c074aa5 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -43,7 +43,11 @@ const onErrorRetry = ( } // Default cache provider -const [cache, mutate] = initCache(new Map()) as [Cache, ScopedMutator] +const [cache, mutate] = initCache(new Map()) as [ + Cache, + ScopedMutator, + () => {} +] export { cache, mutate } // Default config diff --git a/src/utils/web-preset.ts b/src/utils/web-preset.ts index 583ce9dc7..64c179bb4 100644 --- a/src/utils/web-preset.ts +++ b/src/utils/web-preset.ts @@ -14,6 +14,8 @@ const isOnline = () => online // For node and React Native, `add/removeEventListener` doesn't exist on window. const onWindowEvent = (hasWindow && addEventListener) || noop const onDocumentEvent = (hasDocument && document.addEventListener) || noop +const offWindowEvent = (hasWindow && removeEventListener) || noop +const offDocumentEvent = (hasDocument && document.removeEventListener) || noop const isVisible = () => { const visibilityState = hasDocument && document.visibilityState @@ -27,18 +29,28 @@ const initFocus = (cb: () => void) => { // focus revalidate onDocumentEvent('visibilitychange', cb) onWindowEvent('focus', cb) + return () => { + offDocumentEvent('visibilitychange', cb) + offWindowEvent('focus', cb) + } } const initReconnect = (cb: () => void) => { - // reconnect revalidate - onWindowEvent('online', () => { + // revalidate on reconnected + const onOnline = () => { online = true cb() - }) + } // nothing to revalidate, just update the status - onWindowEvent('offline', () => { + const onOffline = () => { online = false - }) + } + onWindowEvent('online', onOnline) + onWindowEvent('offline', onOffline) + return () => { + offWindowEvent('online', onOnline) + offWindowEvent('offline', onOffline) + } } export const preset = { diff --git a/test/use-swr-cache.test.tsx b/test/use-swr-cache.test.tsx index a04a6809a..22b7b6234 100644 --- a/test/use-swr-cache.test.tsx +++ b/test/use-swr-cache.test.tsx @@ -151,6 +151,8 @@ describe('useSWR - cache provider', () => { it('should respect provider options', async () => { const key = createKey() const focusFn = jest.fn() + const unsubscribeFocusFn = jest.fn() + const unsubscribeReconnectFn = jest.fn() let value = 1 function Foo() { @@ -166,9 +168,11 @@ describe('useSWR - cache provider', () => { provider: () => new Map([[key, 0]]), initFocus() { focusFn() + return unsubscribeFocusFn }, initReconnect() { /* do nothing */ + return unsubscribeReconnectFn } }} > @@ -176,7 +180,7 @@ describe('useSWR - cache provider', () => { ) } - render() + const { unmount } = render() screen.getByText('0') // mount @@ -186,7 +190,10 @@ describe('useSWR - cache provider', () => { await focusOn(window) // revalidateOnFocus won't work screen.getByText('1') + unmount() expect(focusFn).toBeCalled() + expect(unsubscribeFocusFn).toBeCalledTimes(1) + expect(unsubscribeReconnectFn).toBeCalledTimes(1) }) it('should work with revalidateOnFocus', async () => { diff --git a/test/use-swr-concurrent-rendering.test.tsx b/test/use-swr-concurrent-rendering.test.tsx index 85becb768..241041810 100644 --- a/test/use-swr-concurrent-rendering.test.tsx +++ b/test/use-swr-concurrent-rendering.test.tsx @@ -9,7 +9,7 @@ import { createResponse, sleep } from './utils' describe('useSWR - concurrent rendering', () => { let React, ReactDOM, act, useSWR - beforeEach(() => { + beforeAll(() => { jest.resetModules() jest.mock('scheduler', () => require('scheduler/unstable_mock')) jest.mock('react', () => require('react-experimental'))