Skip to content
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

fix(infinite): Fix the ability to use preload along with useSWRInfinite #2723

Merged
merged 3 commits into from
Jul 26, 2023
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
5 changes: 1 addition & 4 deletions _internal/src/constants.ts
Original file line number Diff line number Diff line change
@@ -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$'
4 changes: 4 additions & 0 deletions _internal/src/events.ts
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions _internal/src/index.react-server.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { serialize } from './utils/serialize'
export { INFINITE_PREFIX } from './constants'
5 changes: 3 additions & 2 deletions _internal/src/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
2 changes: 1 addition & 1 deletion _internal/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type * as revalidateEvents from './constants'
import type * as revalidateEvents from './events'

export type GlobalState = [
Record<string, RevalidateCallback[]>, // EVENT_REVALIDATORS
Expand Down
2 changes: 1 addition & 1 deletion _internal/src/utils/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion _internal/src/utils/mutate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
11 changes: 9 additions & 2 deletions _internal/src/utils/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions infinite/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
createCacheHelper,
useIsomorphicLayoutEffect,
serialize,
withMiddleware
withMiddleware,
INFINITE_PREFIX
} from 'swr/_internal'
import type {
BareFetcher,
Expand All @@ -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<undefined>
Expand Down
4 changes: 1 addition & 3 deletions infinite/src/serialize.ts
Original file line number Diff line number Diff line change
@@ -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]
Expand Down
239 changes: 239 additions & 0 deletions test/use-swr-infinite-preload.test.tsx
Original file line number Diff line number Diff line change
@@ -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 <div>data:{data}</div>
}

preload(getKey(0), fetcher)
expect(fetcher).toBeCalledTimes(1)

renderWithConfig(<Page />)
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 <div>data:{data}</div>
}

preload(getKey(0), fetcher)
preload(getKey(0), fetcher)
preload(getKey(0), fetcher)
expect(fetcher).toBeCalledTimes(1)

renderWithConfig(<Page />)
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 <div>data:{data}</div>
}

function Page() {
const [show, setShow] = useState(false)
useEffect(() => {
preload(getKey(0), fetcher)
}, [])
return show ? (
<Comp />
) : (
<button onClick={() => setShow(true)}>click</button>
)
}

renderWithConfig(<Page />)
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 <div>data:{data}</div>
}

preload(getKey(0), fetcher)
expect(fetcher).toBeCalledTimes(1)

renderWithConfig(
<Suspense
fallback={
<Profiler id={key} onRender={onRender}>
loading
</Profiler>
}
>
<Page />
</Suspense>
)
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 (
<div>
data:{data1}:{data2}
</div>
)
}

preload(getKey1(0), fetcher1)
preload(getKey1(0), fetcher2)

renderWithConfig(
<Suspense fallback="loading">
<Page />
</Suspense>
)
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<any>(getKey, fetcher)
mutate = swr.mutate

if (error) {
return <div>error:{error.message}</div>
}
return <div>data:{data}</div>
}

try {
// error
await preload(getKey(0), fetcher)
} catch (e) {
// noop
}

renderWithConfig(<Page />)
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 (
<Profiler id={key} onRender={onRender}>
data:{data}
</Profiler>
)
}

preload(getKey(0), fetcher)
expect(fetcher).toBeCalledTimes(1)

const { rerender } = renderWithConfig(<Page />)
expect(onRender).toBeCalledTimes(1)
// rerender when the preloading is in-flight, and the deduping interval is over
await act(() => sleep(10))
rerender(<Page />)
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))
})
})