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: improve preload and suspense integration #2658

Merged
merged 12 commits into from
Jun 15, 2023
6 changes: 6 additions & 0 deletions _internal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ export type Fetcher<
? (arg: Arg) => FetcherResponse<Data>
: never

export type ReactUsePromise<T = unknown, Error = unknown> = Promise<any> & {
status?: 'pending' | 'fulfilled' | 'rejected'
value?: T
reason?: Error
}

export type BlockingData<
Data = any,
Options = SWROptions<Data>
Expand Down
2 changes: 2 additions & 0 deletions _internal/utils/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export const isFunction = <
v: unknown
): v is T => typeof v == 'function'
export const mergeObjects = (a: any, b?: any) => ({ ...a, ...b })
export const isPromiseLike = (x: unknown): x is PromiseLike<unknown> =>
isFunction((x as any).then)

const STR_UNDEFINED = 'undefined'

Expand Down
11 changes: 6 additions & 5 deletions _internal/utils/mutate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
isFunction,
isUndefined,
UNDEFINED,
mergeObjects
mergeObjects,
isPromiseLike
} from './helper'
import { SWRGlobalState } from './global-state'
import { getTimestamp } from './timestamp'
Expand Down Expand Up @@ -73,8 +74,7 @@ export async function internalMutate<Data>(
const keyFilter = _key
const matchedKeys: Key[] = []
const it = cache.keys()
for (let keyIt = it.next(); !keyIt.done; keyIt = it.next()) {
const key = keyIt.value
for (const key of it) {
promer94 marked this conversation as resolved.
Show resolved Hide resolved
if (
// Skip the special useSWRInfinite and useSWRSubscription keys.
!/^\$(inf|sub)\$/.test(key) &&
Expand All @@ -93,7 +93,7 @@ export async function internalMutate<Data>(
const [key] = serialize(_k)
if (!key) return
const [get, set] = createCacheHelper<Data, MutateState<Data>>(cache, key)
const [EVENT_REVALIDATORS, MUTATION, FETCH] = SWRGlobalState.get(
const [EVENT_REVALIDATORS, MUTATION, FETCH, PRELOAD] = SWRGlobalState.get(
cache
) as GlobalState

Expand All @@ -103,6 +103,7 @@ export async function internalMutate<Data>(
// Invalidate the key by deleting the concurrent request markers so new
// requests will not be deduped.
delete FETCH[key]
delete PRELOAD[key]
if (revalidators && revalidators[0]) {
return revalidators[0](revalidateEvents.MUTATE_EVENT).then(
() => get().data
Expand Down Expand Up @@ -156,7 +157,7 @@ export async function internalMutate<Data>(
}

// `data` is a promise/thenable, resolve the final data first.
if (data && isFunction((data as Promise<Data>).then)) {
if (data && isPromiseLike(data)) {
// This means that the mutation is async, we need to check timestamps to
// avoid race conditions.
data = await (data as Promise<Data>).catch(err => {
Expand Down
10 changes: 4 additions & 6 deletions _internal/utils/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {
import { serialize } from './serialize'
import { cache } from './config'
import { SWRGlobalState } from './global-state'

import { isUndefined } from './helper'
// Basically same as Fetcher but without Conditional Fetching
type PreloadFetcher<
Data = unknown,
Expand Down Expand Up @@ -47,11 +47,9 @@ export const middleware: Middleware =
const [key] = serialize(key_)
const [, , , PRELOAD] = SWRGlobalState.get(cache) as GlobalState
const req = PRELOAD[key]
if (req) {
delete PRELOAD[key]
return req
}
return fetcher_(...args)
if (isUndefined(req)) return fetcher_(...args)
delete PRELOAD[key]
return req
})
return useSWRNext(key_, fetcher, config)
}
18 changes: 10 additions & 8 deletions core/use-swr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ import type {
SWRHook,
RevalidateEvent,
StateDependencies,
GlobalState
GlobalState,
ReactUsePromise
} from 'swr/_internal'

const use =
Expand Down Expand Up @@ -105,7 +106,7 @@ export const useSWRHandler = <Data = any, Error = any>(
keepPreviousData
} = config

const [EVENT_REVALIDATORS, MUTATION, FETCH] = SWRGlobalState.get(
const [EVENT_REVALIDATORS, MUTATION, FETCH, PRELOAD] = SWRGlobalState.get(
cache
) as GlobalState

Expand Down Expand Up @@ -615,7 +616,7 @@ export const useSWRHandler = <Data = any, Error = any>(
// Keep the original key in the cache.
setCache({ _k: fnArg })

// Trigger a revalidation.
// Trigger a revalidation
if (shouldDoInitialRevalidation) {
if (isUndefined(data) || IS_SERVER) {
// Revalidate immediately.
Expand Down Expand Up @@ -698,13 +699,14 @@ export const useSWRHandler = <Data = any, Error = any>(
fetcherRef.current = fetcher
configRef.current = config
unmountedRef.current = false
const req = PRELOAD[key]
if (!isUndefined(req)) {
const promise = boundMutate(req)
use(promise)
}

if (isUndefined(error)) {
const promise: Promise<boolean> & {
status?: 'pending' | 'fulfilled' | 'rejected'
value?: boolean
reason?: unknown
} = revalidate(WITH_DEDUPE)
const promise: ReactUsePromise<boolean> = revalidate(WITH_DEDUPE)
if (!isUndefined(returnedData)) {
promise.status = 'fulfilled'
promise.value = true
Expand Down
10 changes: 0 additions & 10 deletions e2e/site/app/head.tsx

This file was deleted.

14 changes: 14 additions & 0 deletions e2e/site/app/suspense-after-preload/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Suspense } from 'react'
import dynamic from 'next/dynamic'

const RemoteData = dynamic(() => import('./remote-data'), {
ssr: false
})

export default function HomePage() {
return (
<Suspense fallback={<div>loading component</div>}>
<RemoteData></RemoteData>
</Suspense>
)
}
44 changes: 44 additions & 0 deletions e2e/site/app/suspense-after-preload/remote-data.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use client'
import { Suspense, useState } from 'react'
import useSWR from 'swr'
import { preload } from 'swr'

const fetcher = ([key, delay]: [key: string, delay: number]) =>
new Promise<string>(r => {
setTimeout(r, delay, key)
})

const key = ['suspense-after-preload', 300] as const
const useRemoteData = () =>
useSWR(key, fetcher, {
suspense: true
})

const Demo = () => {
const { data } = useRemoteData()
return <div>{data}</div>
}

function Comp() {
const [show, toggle] = useState(false)

return (
<div className="App">
<button
onClick={async () => {
preload(key, fetcher)
toggle(!show)
}}
>
preload
</button>
{show ? (
<Suspense fallback={<div>loading</div>}>
<Demo />
</Suspense>
) : null}
</div>
)
}

export default Comp
43 changes: 43 additions & 0 deletions e2e/site/app/suspense-retry-18-3/manual-retry.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use client'
import { Suspense } from 'react'
import { ErrorBoundary } from 'react-error-boundary'
import { useRemoteData, preloadRemote } from './use-remote-data'

const Demo = () => {
const { data } = useRemoteData()
return <div>data: {data}</div>
}

function Fallback({ resetErrorBoundary }: any) {
return (
<div role="alert">
<p>Something went wrong</p>
<button
onClick={() => {
resetErrorBoundary()
}}
>
retry
</button>
</div>
)
}

function RemoteData() {
return (
<div className="App">
<ErrorBoundary
FallbackComponent={Fallback}
onReset={() => {
preloadRemote()
}}
>
<Suspense fallback={<div>loading</div>}>
<Demo />
</Suspense>
</ErrorBoundary>
</div>
)
}

export default RemoteData
14 changes: 14 additions & 0 deletions e2e/site/app/suspense-retry-18-3/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Suspense } from 'react'
import dynamic from 'next/dynamic'

const RemoteData = dynamic(() => import('./manual-retry'), {
ssr: false
})

export default function HomePage() {
return (
<Suspense fallback={<div>loading component</div>}>
<RemoteData></RemoteData>
</Suspense>
)
}
21 changes: 21 additions & 0 deletions e2e/site/app/suspense-retry-18-3/use-remote-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use client'
import useSWR from 'swr'
import { preload } from 'swr'

let count = 0
const fetcher = () => {
count++
if (count === 1) return Promise.reject('wrong')
return fetch('/api/retry')
.then(r => r.json())
.then(r => r.name)
}

const key = 'manual-retry-18-3'

export const useRemoteData = () =>
useSWR(key, fetcher, {
suspense: true
})

export const preloadRemote = () => preload(key, fetcher)
54 changes: 54 additions & 0 deletions e2e/site/component/manual-retry-mutate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Suspense } from 'react'
import { ErrorBoundary } from 'react-error-boundary'
import useSWR from 'swr'
import { mutate } from 'swr'

let count = 0
export const fetcher = () => {
count++
if (count === 1) return Promise.reject('wrong')
return fetch('/api/retry')
.then(r => r.json())
.then(r => r.name)
}

const key = 'manual-retry-mutate'

export const useRemoteData = () =>
useSWR(key, fetcher, {
suspense: true
})
const Demo = () => {
const { data } = useRemoteData()
return <div>data: {data}</div>
}

function Fallback({ resetErrorBoundary }: any) {
return (
<div role="alert">
<p>Something went wrong</p>
<button
onClick={async () => {
await mutate(key, fetcher)
resetErrorBoundary()
}}
>
retry
</button>
</div>
)
}

function RemoteData() {
return (
<div className="App">
<ErrorBoundary FallbackComponent={Fallback}>
<Suspense fallback={<div>loading</div>}>
<Demo />
</Suspense>
</ErrorBoundary>
</div>
)
}

export default RemoteData
42 changes: 42 additions & 0 deletions e2e/site/component/manual-retry.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Suspense } from 'react'
import { ErrorBoundary } from 'react-error-boundary'
import { useRemoteData, preloadRemote } from './use-remote-data'

const Demo = () => {
const { data } = useRemoteData()
return <div>data: {data}</div>
}

function Fallback({ resetErrorBoundary }: any) {
return (
<div role="alert">
<p>Something went wrong</p>
<button
onClick={() => {
resetErrorBoundary()
}}
>
retry
</button>
</div>
)
}

function RemoteData() {
return (
<div className="App">
<ErrorBoundary
FallbackComponent={Fallback}
onReset={() => {
preloadRemote()
}}
>
<Suspense fallback={<div>loading</div>}>
<Demo />
</Suspense>
</ErrorBoundary>
</div>
)
}

export default RemoteData
Loading