diff --git a/_internal/src/constants.ts b/_internal/src/constants.ts index b055b8067..32d45ce11 100644 --- a/_internal/src/constants.ts +++ b/_internal/src/constants.ts @@ -1,4 +1 @@ -export const FOCUS_EVENT = 0 -export const RECONNECT_EVENT = 1 -export const MUTATE_EVENT = 2 -export const ERROR_REVALIDATE_EVENT = 3 +export const INFINITE_PREFIX = '$inf$' diff --git a/_internal/src/events.ts b/_internal/src/events.ts new file mode 100644 index 000000000..b055b8067 --- /dev/null +++ b/_internal/src/events.ts @@ -0,0 +1,4 @@ +export const FOCUS_EVENT = 0 +export const RECONNECT_EVENT = 1 +export const MUTATE_EVENT = 2 +export const ERROR_REVALIDATE_EVENT = 3 diff --git a/_internal/src/index.react-server.ts b/_internal/src/index.react-server.ts index 52fd9e5cc..f00371d78 100644 --- a/_internal/src/index.react-server.ts +++ b/_internal/src/index.react-server.ts @@ -1 +1,2 @@ export { serialize } from './utils/serialize' +export { INFINITE_PREFIX } from './constants' diff --git a/_internal/src/index.ts b/_internal/src/index.ts index 750d62fed..dfe9a8148 100644 --- a/_internal/src/index.ts +++ b/_internal/src/index.ts @@ -1,7 +1,8 @@ import SWRConfig from './utils/config-context' -import * as revalidateEvents from './constants' +import * as revalidateEvents from './events' +import { INFINITE_PREFIX } from './constants' -export { SWRConfig, revalidateEvents } +export { SWRConfig, revalidateEvents, INFINITE_PREFIX } export { initCache } from './utils/cache' export { defaultConfig, cache, mutate, compare } from './utils/config' diff --git a/_internal/src/types.ts b/_internal/src/types.ts index 385dfbf66..4d0aef278 100644 --- a/_internal/src/types.ts +++ b/_internal/src/types.ts @@ -1,4 +1,4 @@ -import type * as revalidateEvents from './constants' +import type * as revalidateEvents from './events' export type GlobalState = [ Record, // EVENT_REVALIDATORS diff --git a/_internal/src/utils/cache.ts b/_internal/src/utils/cache.ts index f432c4c1d..23b977458 100644 --- a/_internal/src/utils/cache.ts +++ b/_internal/src/utils/cache.ts @@ -3,7 +3,7 @@ import { IS_SERVER } from './env' import { UNDEFINED, mergeObjects, noop } from './shared' import { internalMutate } from './mutate' import { SWRGlobalState } from './global-state' -import * as revalidateEvents from '../constants' +import * as revalidateEvents from '../events' import type { Cache, diff --git a/_internal/src/utils/mutate.ts b/_internal/src/utils/mutate.ts index a9d6eff84..4667b1b7c 100644 --- a/_internal/src/utils/mutate.ts +++ b/_internal/src/utils/mutate.ts @@ -9,7 +9,7 @@ import { } from './shared' import { SWRGlobalState } from './global-state' import { getTimestamp } from './timestamp' -import * as revalidateEvents from '../constants' +import * as revalidateEvents from '../events' import type { Cache, MutatorCallback, diff --git a/_internal/src/utils/preload.ts b/_internal/src/utils/preload.ts index 80e188843..75e69d716 100644 --- a/_internal/src/utils/preload.ts +++ b/_internal/src/utils/preload.ts @@ -9,6 +9,7 @@ import { serialize } from './serialize' import { cache } from './config' import { SWRGlobalState } from './global-state' import { isUndefined } from './shared' +import { INFINITE_PREFIX } from '../constants' // Basically same as Fetcher but without Conditional Fetching type PreloadFetcher< Data = unknown, @@ -46,9 +47,15 @@ export const middleware: Middleware = ((...args: any[]) => { const [key] = serialize(key_) const [, , , PRELOAD] = SWRGlobalState.get(cache) as GlobalState - const req = PRELOAD[key] + + let normalizedKey = key + if (key.startsWith(INFINITE_PREFIX)) { + normalizedKey = key.slice(INFINITE_PREFIX.length) + } + + const req = PRELOAD[normalizedKey] if (isUndefined(req)) return fetcher_(...args) - delete PRELOAD[key] + delete PRELOAD[normalizedKey] return req }) return useSWRNext(key_, fetcher, config) diff --git a/infinite/src/index.ts b/infinite/src/index.ts index dfc76df81..7fd4b6ff5 100644 --- a/infinite/src/index.ts +++ b/infinite/src/index.ts @@ -11,7 +11,8 @@ import { createCacheHelper, useIsomorphicLayoutEffect, serialize, - withMiddleware + withMiddleware, + INFINITE_PREFIX } from 'swr/_internal' import type { BareFetcher, @@ -30,7 +31,7 @@ import type { SWRInfiniteCompareFn } from './types' import { useSyncExternalStore } from 'use-sync-external-store/shim/index.js' -import { INFINITE_PREFIX, getFirstPageKey } from './serialize' +import { getFirstPageKey } from './serialize' // const INFINITE_PREFIX = '$inf$' const EMPTY_PROMISE = Promise.resolve() as Promise diff --git a/infinite/src/serialize.ts b/infinite/src/serialize.ts index 53f6af0e0..7f2e0c9d8 100644 --- a/infinite/src/serialize.ts +++ b/infinite/src/serialize.ts @@ -1,7 +1,5 @@ import type { SWRInfiniteKeyLoader } from './types' -import { serialize } from 'swr/_internal' - -export const INFINITE_PREFIX = '$inf$' +import { serialize, INFINITE_PREFIX } from 'swr/_internal' export const getFirstPageKey = (getKey: SWRInfiniteKeyLoader) => { return serialize(getKey ? getKey(0, null) : null)[0] diff --git a/test/use-swr-infinite-preload.test.tsx b/test/use-swr-infinite-preload.test.tsx new file mode 100644 index 000000000..b47508380 --- /dev/null +++ b/test/use-swr-infinite-preload.test.tsx @@ -0,0 +1,239 @@ +import { act, fireEvent, screen } from '@testing-library/react' +import { Suspense, useEffect, useState, Profiler } from 'react' +import { preload } from 'swr' +import useSWRInfinite from 'swr/infinite' +import { createKey, createResponse, renderWithConfig, sleep } from './utils' + +describe('useSWRInfinite - preload', () => { + const getKeyFunction = (key: string) => (index: number) => + `page-${index}-${key}` + + it('preload the fetcher function', async () => { + const key = createKey() + const getKey = getKeyFunction(key) + + const fetcher = jest.fn(() => createResponse('foo')) + function Page() { + const { data } = useSWRInfinite(getKey, fetcher) + return
data:{data}
+ } + + preload(getKey(0), fetcher) + expect(fetcher).toBeCalledTimes(1) + + renderWithConfig() + await screen.findByText('data:foo') + expect(fetcher).toBeCalledTimes(1) + }) + + it('should avoid preloading the resource multiple times', async () => { + const key = createKey() + const getKey = getKeyFunction(key) + const fetcher = jest.fn(() => createResponse('foo')) + + function Page() { + const { data } = useSWRInfinite(getKey, fetcher) + return
data:{data}
+ } + + preload(getKey(0), fetcher) + preload(getKey(0), fetcher) + preload(getKey(0), fetcher) + expect(fetcher).toBeCalledTimes(1) + + renderWithConfig() + await screen.findByText('data:foo') + expect(fetcher).toBeCalledTimes(1) + }) + + it('should be able to prealod resources in effects', async () => { + const key = createKey() + const getKey = getKeyFunction(key) + const fetcher = jest.fn(() => createResponse('foo')) + + function Comp() { + const { data } = useSWRInfinite(getKey, fetcher) + return
data:{data}
+ } + + function Page() { + const [show, setShow] = useState(false) + useEffect(() => { + preload(getKey(0), fetcher) + }, []) + return show ? ( + + ) : ( + + ) + } + + renderWithConfig() + expect(fetcher).toBeCalledTimes(1) + + fireEvent.click(screen.getByText('click')) + + await screen.findByText('data:foo') + expect(fetcher).toBeCalledTimes(1) + }) + + it('preload the fetcher function with the suspense mode', async () => { + const key = createKey() + const getKey = getKeyFunction(key) + const fetcher = jest.fn(() => createResponse('foo')) + const onRender = jest.fn() + function Page() { + const { data } = useSWRInfinite(getKey, fetcher, { suspense: true }) + return
data:{data}
+ } + + preload(getKey(0), fetcher) + expect(fetcher).toBeCalledTimes(1) + + renderWithConfig( + + loading + + } + > + + + ) + await screen.findByText('data:foo') + expect(onRender).toBeCalledTimes(1) + expect(fetcher).toBeCalledTimes(1) + }) + + it('avoid suspense waterfall by prefetching the resources', async () => { + const key1 = createKey() + const getKey1 = getKeyFunction(key1) + const key2 = createKey() + const getKey2 = getKeyFunction(key2) + + const response1 = createResponse('foo', { delay: 50 }) + const response2 = createResponse('bar', { delay: 50 }) + + const fetcher1 = () => response1 + const fetcher2 = () => response2 + + function Page() { + const { data: data1 } = useSWRInfinite(getKey1, fetcher1, { + suspense: true + }) + const { data: data2 } = useSWRInfinite(getKey2, fetcher2, { + suspense: true + }) + + return ( +
+ data:{data1}:{data2} +
+ ) + } + + preload(getKey1(0), fetcher1) + preload(getKey1(0), fetcher2) + + renderWithConfig( + + + + ) + screen.getByText('loading') + // Should avoid waterfall(50ms + 50ms) + await act(() => sleep(80)) + screen.getByText('data:foo:bar') + }) + + it('reset the preload result when the preload function gets an error', async () => { + const key = createKey() + const getKey = getKeyFunction(key) + let count = 0 + + const fetcher = () => { + ++count + const res = count === 1 ? new Error('err') : 'foo' + return createResponse(res) + } + + let mutate + function Page() { + const { data, error, ...swr } = useSWRInfinite(getKey, fetcher) + mutate = swr.mutate + + if (error) { + return
error:{error.message}
+ } + return
data:{data}
+ } + + try { + // error + await preload(getKey(0), fetcher) + } catch (e) { + // noop + } + + renderWithConfig() + screen.getByText('data:') + + // use the preloaded result + await screen.findByText('error:err') + expect(count).toBe(1) + + // revalidate + await act(() => mutate(getKey(0))) + // should not use the preload data + await screen.findByText('data:foo') + }) + + it('dedupe requests during preloading', async () => { + const key = createKey() + const getKey = getKeyFunction(key) + + const fetcher = jest.fn(() => + createResponse('foo', { + delay: 50 + }) + ) + const onRender = jest.fn() + + function Page() { + const { data } = useSWRInfinite(getKey, fetcher, { dedupingInterval: 0 }) + return ( + + data:{data} + + ) + } + + preload(getKey(0), fetcher) + expect(fetcher).toBeCalledTimes(1) + + const { rerender } = renderWithConfig() + expect(onRender).toBeCalledTimes(1) + // rerender when the preloading is in-flight, and the deduping interval is over + await act(() => sleep(10)) + rerender() + expect(onRender).toBeCalledTimes(2) + + await screen.findByText('data:foo') + expect(fetcher).toBeCalledTimes(1) + expect(onRender).toBeCalledTimes(3) + }) + + it('should pass serialize key to fetcher', async () => { + const key = createKey() + const getKey = getKeyFunction(key) + let calledWith: string + + const fetcher = (args: string) => { + calledWith = args + } + + preload(() => getKey(0), fetcher) + expect(calledWith).toBe(getKey(0)) + }) +})