diff --git a/src/types.ts b/src/types.ts index a0c3607f1..d73fd1c39 100644 --- a/src/types.ts +++ b/src/types.ts @@ -49,8 +49,6 @@ export interface Configuration< isOnline: () => boolean isDocumentVisible: () => boolean - registerOnFocus: (cb: () => void) => void - registerOnReconnect: (cb: () => void) => void /** * @deprecated `revalidateOnMount` will be removed. Please considering using the `revalidateWhenStale` option. @@ -58,6 +56,11 @@ export interface Configuration< revalidateOnMount?: boolean } +export type ProviderOptions = { + setupOnFocus: (cb: () => void) => void + setupOnReconnect: (cb: () => void) => void +} + export type SWRHook = ( ...args: | readonly [Key] diff --git a/src/use-swr.ts b/src/use-swr.ts index 1089725bd..e09ca1b78 100644 --- a/src/use-swr.ts +++ b/src/use-swr.ts @@ -1,5 +1,6 @@ import { useCallback, useRef, useDebugValue } from 'react' import defaultConfig from './utils/config' +import { provider as defaultProvider } from './utils/web-preset' import { wrapCache } from './utils/cache' import { IS_SERVER, rAF, useIsomorphicLayoutEffect } from './utils/env' import { serialize } from './utils/serialize' @@ -20,7 +21,8 @@ import { SWRConfiguration, Cache, ScopedMutator, - SWRHook + SWRHook, + ProviderOptions } from './types' type Revalidator = (...args: any[]) => void @@ -45,22 +47,23 @@ const getGlobalState = (cache: Cache) => { ] } -// Setup DOM events listeners for `focus` and `reconnect` actions -if (!IS_SERVER) { - const [FOCUS_REVALIDATORS, RECONNECT_REVALIDATORS] = getGlobalState( - defaultConfig.cache - ) +function setupGlobalEvents(cache: Cache, _opts: Partial = {}) { + if (IS_SERVER) return + const opts = { ...defaultProvider, ..._opts } + const [FOCUS_REVALIDATORS, RECONNECT_REVALIDATORS] = getGlobalState(cache) const revalidate = (revalidators: Record) => { - if (!defaultConfig.isDocumentVisible() || !defaultConfig.isOnline()) return - for (const key in revalidators) { if (revalidators[key][0]) revalidators[key][0]() } } - defaultConfig.registerOnFocus(() => revalidate(FOCUS_REVALIDATORS)) - defaultConfig.registerOnReconnect(() => revalidate(RECONNECT_REVALIDATORS)) + + opts.setupOnFocus(() => revalidate(FOCUS_REVALIDATORS)) + opts.setupOnReconnect(() => revalidate(RECONNECT_REVALIDATORS)) } +// Setup DOM events listeners for `focus` and `reconnect` actions +setupGlobalEvents(defaultConfig.cache) + const broadcastState: Broadcaster = ( cache: Cache, key, @@ -522,10 +525,13 @@ export function useSWRHandler( } } + const isVisible = () => + configRef.current.isDocumentVisible() && configRef.current.isOnline() + // Add event listeners. let pending = false const onFocus = () => { - if (configRef.current.revalidateOnFocus && !pending) { + if (configRef.current.revalidateOnFocus && !pending && isVisible()) { pending = true softRevalidate() setTimeout( @@ -536,7 +542,7 @@ export function useSWRHandler( } const onReconnect = () => { - if (configRef.current.revalidateOnReconnect) { + if (configRef.current.revalidateOnReconnect && isVisible()) { softRevalidate() } } @@ -689,12 +695,14 @@ export const mutate = internalMutate.bind( ) as ScopedMutator export function createCache( - provider: Cache + provider: Cache, + options?: Partial ): { cache: Cache mutate: ScopedMutator } { const cache = wrapCache(provider) + setupGlobalEvents(cache, options) return { cache, mutate: internalMutate.bind(null, cache) as ScopedMutator diff --git a/src/utils/config.ts b/src/utils/config.ts index 9b821513d..1a30c5445 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -1,7 +1,7 @@ import { dequal } from 'dequal/lite' import { wrapCache } from './cache' -import webPreset from './web-preset' +import { preset } from './web-preset' import { slowConnection } from './env' import { Configuration, RevalidatorOptions, Revalidator } from '../types' import { UNDEFINED } from './helper' @@ -17,7 +17,7 @@ function onErrorRetry( revalidate: Revalidator, opts: Required ): void { - if (!webPreset.isDocumentVisible()) { + if (!preset.isDocumentVisible()) { // If it's hidden, stop. It will auto revalidate when refocusing. return } @@ -61,8 +61,8 @@ const defaultConfig: Configuration = { isPaused: () => false, cache: wrapCache(new Map()), - // presets - ...webPreset + // use web preset by default + ...preset } as const export default defaultConfig diff --git a/src/utils/web-preset.ts b/src/utils/web-preset.ts index 80882861b..7aae7d4ab 100644 --- a/src/utils/web-preset.ts +++ b/src/utils/web-preset.ts @@ -1,3 +1,4 @@ +import { ProviderOptions } from '../types' import { isUndefined } from './helper' /** @@ -9,52 +10,47 @@ import { isUndefined } from './helper' */ let online = true const isOnline = () => online +const hasWindow = typeof window !== 'undefined' +const hasDocument = typeof document !== 'undefined' +const add = 'addEventListener' +function noop() {} -// For node and React Native, `window.addEventListener` doesn't exist. -const addWindowEventListener = - typeof window !== 'undefined' && !isUndefined(window.addEventListener) - ? window.addEventListener.bind(window) - : null -const addDocumentEventListener = - typeof document !== 'undefined' - ? document.addEventListener.bind(document) - : null +// For node and React Native, `add/removeEventListener` doesn't exist on window. +const onWindowEvent = hasWindow && window[add] ? window[add] : noop +const onDocumentEvent = hasDocument ? document[add] : noop const isDocumentVisible = () => { - if (addDocumentEventListener) { - const visibilityState = document.visibilityState - if (!isUndefined(visibilityState)) { - return visibilityState !== 'hidden' - } + const visibilityState = hasDocument && document.visibilityState + if (!isUndefined(visibilityState)) { + return visibilityState !== 'hidden' } - // always assume it's visible return true } -const registerOnFocus = (cb: () => void) => { - if (addWindowEventListener && addDocumentEventListener) { - // focus revalidate - addDocumentEventListener('visibilitychange', cb) - addWindowEventListener('focus', cb) - } +const setupOnFocus = (cb: () => void) => { + // focus revalidate + onDocumentEvent('visibilitychange', cb) + onWindowEvent('focus', cb) } -const registerOnReconnect = (cb: () => void) => { - if (addWindowEventListener) { - // reconnect revalidate - addWindowEventListener('online', () => { - online = true - cb() - }) - - // nothing to revalidate, just update the status - addWindowEventListener('offline', () => (online = false)) - } +const setupOnReconnect = (cb: () => void) => { + // reconnect revalidate + onWindowEvent('online', () => { + online = true + cb() + }) + // nothing to revalidate, just update the status + onWindowEvent('offline', () => { + online = false + }) } -export default { +export const preset = { isOnline, - isDocumentVisible, - registerOnFocus, - registerOnReconnect + isDocumentVisible } as const + +export const provider: ProviderOptions = { + setupOnFocus, + setupOnReconnect +} diff --git a/test/index.js b/test/index.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/use-swr-cache.test.tsx b/test/use-swr-cache.test.tsx index 55c78d89a..f2e202b0c 100644 --- a/test/use-swr-cache.test.tsx +++ b/test/use-swr-cache.test.tsx @@ -1,7 +1,7 @@ import { act, fireEvent, render, screen } from '@testing-library/react' import React, { useState } from 'react' import useSWR, { createCache, SWRConfig } from 'swr' -import { sleep, createKey } from './utils' +import { sleep, createKey, nextTick, focusOn } from './utils' describe('useSWR - cache', () => { it('should be able to update the cache', async () => { @@ -137,4 +137,68 @@ describe('useSWR - cache', () => { render() screen.getByText('1:2') }) + + it('should honor createCache provider options', async () => { + const key = createKey() + const provider = new Map([[key, 0]]) + const { cache } = createCache(provider, { + setupOnFocus() { + /* do nothing */ + }, + setupOnReconnect() { + /* do nothing */ + } + }) + let value = 1 + function Foo() { + const { data } = useSWR(key, () => value++, { + dedupingInterval: 0 + }) + return <>{String(data)} + } + function Page() { + return ( + + + + ) + } + render() + screen.getByText('0') + + // mount + await screen.findByText('1') + await nextTick() + // try to trigger revalidation, but shouldn't work + await focusOn(window) + // revalidateOnFocus won't work + screen.getByText('1') + }) + + it('should work with revalidateOnFocus', async () => { + const key = createKey() + const provider = new Map() + const { cache } = createCache(provider) + let value = 0 + function Foo() { + const { data } = useSWR(key, () => value++, { + dedupingInterval: 0 + }) + return <>{String(data)} + } + function Page() { + return ( + + + + ) + } + render() + screen.getByText('undefined') + + await screen.findByText('0') + await nextTick() + await focusOn(window) + screen.getByText('1') + }) }) diff --git a/test/use-swr-focus.test.tsx b/test/use-swr-focus.test.tsx index 7a12195e6..137bd3b51 100644 --- a/test/use-swr-focus.test.tsx +++ b/test/use-swr-focus.test.tsx @@ -1,13 +1,9 @@ import { act, fireEvent, render, screen } from '@testing-library/react' import React, { useState } from 'react' -import useSWR from 'swr' -import { sleep } from './utils' +import useSWR, { createCache } from 'swr' +import { sleep, nextTick as waitForNextTick, focusOn } from './utils' -const waitForNextTick = () => act(() => sleep(1)) -const focusWindow = () => - act(async () => { - fireEvent.focus(window) - }) +const focusWindow = () => focusOn(window) describe('useSWR - focus', () => { it('should revalidate on focus by default', async () => { @@ -57,7 +53,8 @@ describe('useSWR - focus', () => { // should not be revalidated screen.getByText('data: 0') }) - it('revalidateOnFocus shoule be stateful', async () => { + + it('revalidateOnFocus should be stateful', async () => { let value = 0 function Page() { @@ -200,4 +197,26 @@ describe('useSWR - focus', () => { await focusWindow() await screen.findByText('data: 5') }) + + it('should revalidate on focus even with custom cache', async () => { + let value = 0 + const { cache } = createCache(new Map()) + + function Page() { + const { data } = useSWR('revalidateOnFocus + cache', () => value++, { + cache, + revalidateOnFocus: true, + dedupingInterval: 0 + }) + return
data: {data}
+ } + + // reuse default test case + render() + screen.getByText('data:') + await screen.findByText('data: 0') + await waitForNextTick() + await focusWindow() + await screen.findByText('data: 1') + }) }) diff --git a/test/use-swr-immutable.test.tsx b/test/use-swr-immutable.test.tsx index 0cfa7a200..02249c6f2 100644 --- a/test/use-swr-immutable.test.tsx +++ b/test/use-swr-immutable.test.tsx @@ -2,13 +2,9 @@ import { render, screen, act, fireEvent } from '@testing-library/react' import React, { useState } from 'react' import useSWR from 'swr' import useSWRImmutable from 'swr/immutable' -import { sleep, createKey } from './utils' +import { sleep, createKey, nextTick as waitForNextTick, focusOn } from './utils' -const waitForNextTick = () => act(() => sleep(1)) -const focusWindow = () => - act(async () => { - fireEvent.focus(window) - }) +const focusWindow = () => focusOn(window) describe('useSWR - immutable', () => { it('should revalidate on mount', async () => { diff --git a/test/use-swr-integration.test.tsx b/test/use-swr-integration.test.tsx index 4b579677a..585980d4e 100644 --- a/test/use-swr-integration.test.tsx +++ b/test/use-swr-integration.test.tsx @@ -1,9 +1,7 @@ import { act, render, screen, fireEvent } from '@testing-library/react' import React, { useState, useEffect } from 'react' import useSWR from 'swr' -import { createResponse, sleep } from './utils' - -const waitForNextTick = () => act(() => sleep(1)) +import { createResponse, sleep, nextTick as waitForNextTick } from './utils' describe('useSWR', () => { it('should return `undefined` on hydration then return data', async () => { diff --git a/test/use-swr-local-mutation.test.tsx b/test/use-swr-local-mutation.test.tsx index dad6e27ec..cec53f775 100644 --- a/test/use-swr-local-mutation.test.tsx +++ b/test/use-swr-local-mutation.test.tsx @@ -2,9 +2,7 @@ import { act, render, screen, fireEvent } from '@testing-library/react' import React, { useEffect, useState } from 'react' import useSWR, { mutate, createCache, SWRConfig } from 'swr' import { serialize } from '../src/utils/serialize' -import { createResponse, sleep } from './utils' - -const waitForNextTick = () => act(() => sleep(1)) +import { createResponse, sleep, nextTick as waitForNextTick } from './utils' describe('useSWR - local mutation', () => { it('should trigger revalidation programmatically', async () => { diff --git a/test/use-swr-offline.test.tsx b/test/use-swr-offline.test.tsx index cbc80219d..585682025 100644 --- a/test/use-swr-offline.test.tsx +++ b/test/use-swr-offline.test.tsx @@ -1,13 +1,9 @@ -import { act, fireEvent, render, screen } from '@testing-library/react' +import { act, render, screen } from '@testing-library/react' import React from 'react' import useSWR from 'swr' -import { sleep } from './utils' +import { nextTick as waitForNextTick, focusOn } from './utils' -const waitForNextTick = () => act(() => sleep(1)) -const focusWindow = () => - act(async () => { - fireEvent.focus(window) - }) +const focusWindow = () => focusOn(window) const dispatchWindowEvent = event => act(async () => { window.dispatchEvent(new Event(event)) diff --git a/test/use-swr-revalidate.test.tsx b/test/use-swr-revalidate.test.tsx index eaba5a199..00539ebf9 100644 --- a/test/use-swr-revalidate.test.tsx +++ b/test/use-swr-revalidate.test.tsx @@ -1,9 +1,7 @@ import { act, fireEvent, render, screen } from '@testing-library/react' import React from 'react' import useSWR from 'swr' -import { createResponse, sleep } from './utils' - -const waitForNextTick = () => act(() => sleep(1)) +import { createResponse, sleep, nextTick as waitForNextTick } from './utils' describe('useSWR - revalidate', () => { it('should rerender after triggering revalidation', async () => { diff --git a/test/utils.ts b/test/utils.ts index 6d6f66880..ba87d4e7a 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,3 +1,5 @@ +import { act, fireEvent } from '@testing-library/react' + export function sleep(time: number) { return new Promise(resolve => setTimeout(resolve, time)) } @@ -16,4 +18,11 @@ export const createResponse = ( }, delay) ) +export const nextTick = () => act(() => sleep(1)) + +export const focusOn = (element: any) => + act(async () => { + fireEvent.focus(element) + }) + export const createKey = () => 'swr-key-' + ~~(Math.random() * 1e7)