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

feat: add preload function #2026

Merged
merged 17 commits into from
Jun 17, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions _internal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ export { getTimestamp } from './utils/timestamp'
export { useSWRConfig } from './utils/use-swr-config'
export { preset, defaultConfigOptions } from './utils/web-preset'
export { withMiddleware } from './utils/with-middleware'
export { preload } from './utils/preload'

export * from './types'
1 change: 1 addition & 0 deletions _internal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type GlobalState = [
Record<string, RevalidateCallback[]>, // EVENT_REVALIDATORS
Record<string, [number, number]>, // MUTATION: [ts, end_ts]
Record<string, [any, number]>, // FETCH: [data, ts]
Record<string, FetcherResponse<any>>, // PRELOAD
ScopedMutator, // Mutator
(key: string, value: any, prev: any) => void, // Setter
(key: string, callback: (current: any, prev: any) => void) => () => void // Subscriber
Expand Down
3 changes: 2 additions & 1 deletion _internal/utils/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export const initCache = <Data = any>(
EVENT_REVALIDATORS,
{},
{},
{},
mutate,
setter,
subscribe
Expand Down Expand Up @@ -132,5 +133,5 @@ export const initCache = <Data = any>(
return [provider, mutate, initProvider, unmount]
}

return [provider, (SWRGlobalState.get(provider) as GlobalState)[3]]
return [provider, (SWRGlobalState.get(provider) as GlobalState)[4]]
}
4 changes: 2 additions & 2 deletions _internal/utils/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ export const createCacheHelper = <Data = any, T = State<Data, any>>(
// Setter
(info: T) => {
const prev = cache.get(key)
state[4](key as string, mergeObjects(prev, info), prev || EMPTY_CACHE)
state[5](key as string, mergeObjects(prev, info), prev || EMPTY_CACHE)
},
// Subscriber
state[5]
state[6]
] as const
}
3 changes: 3 additions & 0 deletions _internal/utils/middleware-preset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { middleware as preload } from './preload'

export const BUILT_IN_MIDDLEWARE = [preload]
30 changes: 30 additions & 0 deletions _internal/utils/preload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Middleware, Key, BareFetcher, GlobalState } from '../types'
import { serialize } from './serialize'
import { cache } from './config'
import { SWRGlobalState } from './global-state'

export const preload = <Data = any>(key_: Key, fetcher: BareFetcher<Data>) => {
const req = fetcher(key_)
const key = serialize(key_)[0]
const [, , , PRELOAD] = SWRGlobalState.get(cache) as GlobalState
PRELOAD[key] = req
return req
}

export const middleware: Middleware =
useSWRNext => (key_, fetcher_, config) => {
// fetcher might be a sync function, so this should not be an async function
const fetcher =
fetcher_ &&
((...args: any[]) => {
const key = serialize(key_)[0]
const [, , , PRELOAD] = SWRGlobalState.get(cache) as GlobalState
const req = PRELOAD[key]
if (req) {
delete PRELOAD[key]
return req
}
return fetcher_(...args)
})
return useSWRNext(key_, fetcher, config)
}
8 changes: 4 additions & 4 deletions _internal/utils/resolve-args.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { mergeConfigs } from './merge-config'
import { normalize } from './normalize-args'
import { useSWRConfig } from './use-swr-config'
import { BUILT_IN_MIDDLEWARE } from './middleware-preset'

// It's tricky to pass generic types as parameters, so we just directly override
// the types here.
Expand All @@ -18,10 +19,9 @@ export const withArgs = <SWRType>(hook: any) => {
// Apply middleware
let next = hook
const { use } = config
if (use) {
for (let i = use.length; i--; ) {
next = use[i](next)
}
const middleware = (use || []).concat(BUILT_IN_MIDDLEWARE)
for (let i = middleware.length; i--; ) {
next = middleware[i](next)
}

return next(key, fn || config.fetcher, config)
Expand Down
1 change: 1 addition & 0 deletions core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default useSWR
export { SWRConfig, unstable_serialize } from './use-swr'
export { useSWRConfig } from 'swr/_internal'
export { mutate } from 'swr/_internal'
export { preload } from 'swr/_internal'

// Types
export type {
Expand Down
23 changes: 23 additions & 0 deletions test/use-swr-fetcher.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,27 @@ describe('useSWR - fetcher', () => {
// Should fetch with the new fetcher.
await screen.findByText('data:bar')
})

it('should be able to pass falsy values to the fetcher', () => {
const key = createKey()

function Page({ fetcher }) {
const { data } = useSWR(key, fetcher)

return (
<div>
<p>data:{data}</p>
</div>
)
}

const { rerender } = renderWithConfig(<Page fetcher={null} />)
screen.getByText('data:')

rerender(<Page fetcher={undefined} />)
screen.getByText('data:')

rerender(<Page fetcher={false} />)
screen.getByText('data:')
})
})
161 changes: 161 additions & 0 deletions test/use-swr-preload.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { act, screen } from '@testing-library/react'
import React, { Suspense } from 'react'
import useSWR, { preload, useSWRConfig } from 'swr'
import { createKey, createResponse, renderWithConfig, sleep } from './utils'

describe('useSWR - preload', () => {
it('preload the fetcher function', async () => {
const key = createKey()
let count = 0

const fetcher = () => {
++count
return createResponse('foo')
}

function Page() {
const { data } = useSWR(key, fetcher)
return <div>data:{data}</div>
}

preload(key, fetcher)
expect(count).toBe(1)

renderWithConfig(<Page />)
await screen.findByText('data:foo')
expect(count).toBe(1)
})

it('preload the fetcher function with the suspense mode', async () => {
const key = createKey()
let count = 0

const fetcher = () => {
++count
return createResponse('foo')
}

function Page() {
const { data } = useSWR(key, fetcher, { suspense: true })
return <div>data:{data}</div>
}

preload(key, fetcher)
expect(count).toBe(1)

renderWithConfig(
<Suspense fallback="loading">
<Page />
</Suspense>
)
await screen.findByText('data:foo')
expect(count).toBe(1)
})

it('avoid suspense waterfall by prefetching the resources', async () => {
const key1 = createKey()
const key2 = createKey()

const response1 = createResponse('foo', { delay: 50 })
const response2 = createResponse('bar', { delay: 50 })

const fetcher1 = () => response1
const fetcher2 = () => response2

function Page() {
const { data: data1 } = useSWR(key1, fetcher1, { suspense: true })
const { data: data2 } = useSWR(key2, fetcher2, { suspense: true })

return (
<div>
data:{data1}:{data2}
</div>
)
}

preload(key1, fetcher1)
preload(key2, 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()
let count = 0

const fetcher = () => {
++count
const res = count === 1 ? new Error('err') : 'foo'
return createResponse(res)
}

let mutate
function Page() {
mutate = useSWRConfig().mutate
const { data, error } = useSWR<any>(key, fetcher)
if (error) {
return <div>error:{error.message}</div>
}
return <div>data:{data}</div>
}

try {
// error
await preload(key, 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(key))
// should not use the preload data
await screen.findByText('data:foo')
})

it('dedupe requests during preloading', async () => {
const key = createKey()

let fetcherCount = 0
let renderCount = 0

const fetcher = () => {
++fetcherCount
return createResponse('foo', { delay: 50 })
}

function Page() {
++renderCount
const { data } = useSWR(key, fetcher, { dedupingInterval: 0 })
return <div>data:{data}</div>
}

preload(key, fetcher)
expect(fetcherCount).toBe(1)

const { rerender } = renderWithConfig(<Page />)
expect(renderCount).toBe(1)
// rerender when the preloading is in-flight, and the deduping interval is over
await act(() => sleep(10))
rerender(<Page />)
expect(renderCount).toBe(2)

await screen.findByText('data:foo')
expect(fetcherCount).toBe(1)
expect(renderCount).toBe(3)
})
})