Skip to content

Commit

Permalink
feat: improve preload and suspense integration (#2658)
Browse files Browse the repository at this point in the history
  • Loading branch information
promer94 authored Jun 15, 2023
1 parent 7badbb9 commit 387d3d4
Show file tree
Hide file tree
Showing 34 changed files with 540 additions and 90 deletions.
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) {
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

0 comments on commit 387d3d4

Please sign in to comment.