diff --git a/_internal/types.ts b/_internal/types.ts index f2a8b750a..3b399b392 100644 --- a/_internal/types.ts +++ b/_internal/types.ts @@ -25,6 +25,12 @@ export type Fetcher< ? (arg: Arg) => FetcherResponse : never +export type ReactUsePromise = Promise & { + status?: 'pending' | 'fulfilled' | 'rejected' + value?: T + reason?: Error +} + export type BlockingData< Data = any, Options = SWROptions diff --git a/_internal/utils/helper.ts b/_internal/utils/helper.ts index 7356b9426..3c18cb763 100644 --- a/_internal/utils/helper.ts +++ b/_internal/utils/helper.ts @@ -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 => + isFunction((x as any).then) const STR_UNDEFINED = 'undefined' diff --git a/_internal/utils/mutate.ts b/_internal/utils/mutate.ts index b75274db3..36331933d 100644 --- a/_internal/utils/mutate.ts +++ b/_internal/utils/mutate.ts @@ -4,7 +4,8 @@ import { isFunction, isUndefined, UNDEFINED, - mergeObjects + mergeObjects, + isPromiseLike } from './helper' import { SWRGlobalState } from './global-state' import { getTimestamp } from './timestamp' @@ -73,8 +74,7 @@ export async function internalMutate( 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) && @@ -93,7 +93,7 @@ export async function internalMutate( const [key] = serialize(_k) if (!key) return const [get, set] = createCacheHelper>(cache, key) - const [EVENT_REVALIDATORS, MUTATION, FETCH] = SWRGlobalState.get( + const [EVENT_REVALIDATORS, MUTATION, FETCH, PRELOAD] = SWRGlobalState.get( cache ) as GlobalState @@ -103,6 +103,7 @@ export async function internalMutate( // 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 @@ -156,7 +157,7 @@ export async function internalMutate( } // `data` is a promise/thenable, resolve the final data first. - if (data && isFunction((data as Promise).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).catch(err => { diff --git a/_internal/utils/preload.ts b/_internal/utils/preload.ts index cdcbf2751..0843f3833 100644 --- a/_internal/utils/preload.ts +++ b/_internal/utils/preload.ts @@ -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, @@ -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) } diff --git a/core/use-swr.ts b/core/use-swr.ts index 1677d36ac..f04b27259 100644 --- a/core/use-swr.ts +++ b/core/use-swr.ts @@ -39,7 +39,8 @@ import type { SWRHook, RevalidateEvent, StateDependencies, - GlobalState + GlobalState, + ReactUsePromise } from 'swr/_internal' const use = @@ -105,7 +106,7 @@ export const useSWRHandler = ( keepPreviousData } = config - const [EVENT_REVALIDATORS, MUTATION, FETCH] = SWRGlobalState.get( + const [EVENT_REVALIDATORS, MUTATION, FETCH, PRELOAD] = SWRGlobalState.get( cache ) as GlobalState @@ -615,7 +616,7 @@ export const useSWRHandler = ( // 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. @@ -698,13 +699,14 @@ export const useSWRHandler = ( 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 & { - status?: 'pending' | 'fulfilled' | 'rejected' - value?: boolean - reason?: unknown - } = revalidate(WITH_DEDUPE) + const promise: ReactUsePromise = revalidate(WITH_DEDUPE) if (!isUndefined(returnedData)) { promise.status = 'fulfilled' promise.value = true diff --git a/e2e/site/app/head.tsx b/e2e/site/app/head.tsx deleted file mode 100644 index 153f334ce..000000000 --- a/e2e/site/app/head.tsx +++ /dev/null @@ -1,10 +0,0 @@ -export default function Head() { - return ( - <> - SWR E2E Test - - - - - ) -} diff --git a/e2e/site/app/suspense-after-preload/page.tsx b/e2e/site/app/suspense-after-preload/page.tsx new file mode 100644 index 000000000..9e0ceaa82 --- /dev/null +++ b/e2e/site/app/suspense-after-preload/page.tsx @@ -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 ( + loading component}> + + + ) +} diff --git a/e2e/site/app/suspense-after-preload/remote-data.tsx b/e2e/site/app/suspense-after-preload/remote-data.tsx new file mode 100644 index 000000000..1d32818e4 --- /dev/null +++ b/e2e/site/app/suspense-after-preload/remote-data.tsx @@ -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(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
{data}
+} + +function Comp() { + const [show, toggle] = useState(false) + + return ( +
+ + {show ? ( + loading
}> + + + ) : null} + + ) +} + +export default Comp diff --git a/e2e/site/app/suspense-retry-18-3/manual-retry.tsx b/e2e/site/app/suspense-retry-18-3/manual-retry.tsx new file mode 100644 index 000000000..2621ba752 --- /dev/null +++ b/e2e/site/app/suspense-retry-18-3/manual-retry.tsx @@ -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
data: {data}
+} + +function Fallback({ resetErrorBoundary }: any) { + return ( +
+

Something went wrong

+ +
+ ) +} + +function RemoteData() { + return ( +
+ { + preloadRemote() + }} + > + loading
}> + + + + + ) +} + +export default RemoteData diff --git a/e2e/site/app/suspense-retry-18-3/page.tsx b/e2e/site/app/suspense-retry-18-3/page.tsx new file mode 100644 index 000000000..b590f9df8 --- /dev/null +++ b/e2e/site/app/suspense-retry-18-3/page.tsx @@ -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 ( + loading component}> + + + ) +} diff --git a/e2e/site/app/suspense-retry-18-3/use-remote-data.ts b/e2e/site/app/suspense-retry-18-3/use-remote-data.ts new file mode 100644 index 000000000..271810e9b --- /dev/null +++ b/e2e/site/app/suspense-retry-18-3/use-remote-data.ts @@ -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) diff --git a/e2e/site/component/manual-retry-mutate.tsx b/e2e/site/component/manual-retry-mutate.tsx new file mode 100644 index 000000000..aba78ba0b --- /dev/null +++ b/e2e/site/component/manual-retry-mutate.tsx @@ -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
data: {data}
+} + +function Fallback({ resetErrorBoundary }: any) { + return ( +
+

Something went wrong

+ +
+ ) +} + +function RemoteData() { + return ( +
+ + loading
}> + + + + + ) +} + +export default RemoteData diff --git a/e2e/site/component/manual-retry.tsx b/e2e/site/component/manual-retry.tsx new file mode 100644 index 000000000..7de7e8a79 --- /dev/null +++ b/e2e/site/component/manual-retry.tsx @@ -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
data: {data}
+} + +function Fallback({ resetErrorBoundary }: any) { + return ( +
+

Something went wrong

+ +
+ ) +} + +function RemoteData() { + return ( +
+ { + preloadRemote() + }} + > + loading
}> + + + + + ) +} + +export default RemoteData diff --git a/e2e/site/component/use-remote-data.ts b/e2e/site/component/use-remote-data.ts new file mode 100644 index 000000000..8cf96ab57 --- /dev/null +++ b/e2e/site/component/use-remote-data.ts @@ -0,0 +1,20 @@ +import useSWR from 'swr' +import { preload } 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-18-2' + +export const useRemoteData = () => + useSWR(key, fetcher, { + suspense: true + }) + +export const preloadRemote = () => preload(key, fetcher) diff --git a/e2e/site/pages/api/retry.ts b/e2e/site/pages/api/retry.ts new file mode 100644 index 000000000..96b959b3a --- /dev/null +++ b/e2e/site/pages/api/retry.ts @@ -0,0 +1,12 @@ +import type { NextApiRequest, NextApiResponse } from 'next' + +type Data = { + name: string +} + +export default function handler( + req: NextApiRequest, + res: NextApiResponse +) { + res.status(200).json({ name: 'SWR suspense retry works' }) +} diff --git a/e2e/site/pages/initial-render.tsx b/e2e/site/pages/initial-render.tsx deleted file mode 100644 index 6ed1700a8..000000000 --- a/e2e/site/pages/initial-render.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import useSWR from 'swr' -import { Profiler } from 'react' - -const useFetchUser = () => - useSWR( - '/users/100', - url => - new Promise(resolve => { - setTimeout(() => { - resolve(url) - }, 1000) - }) - ) - -function UserSWR() { - useFetchUser() - return
SWRTest
-} - -export default function SWRTest() { - return ( - { - ;(window as any).onRender('UserSWR rendered') - }} - > - - - ) -} diff --git a/e2e/site/pages/suspense-retry-18-2.tsx b/e2e/site/pages/suspense-retry-18-2.tsx new file mode 100644 index 000000000..032295a6c --- /dev/null +++ b/e2e/site/pages/suspense-retry-18-2.tsx @@ -0,0 +1,14 @@ +import { Suspense } from 'react' +import dynamic from 'next/dynamic' + +const RemoteData = dynamic(() => import('../component/manual-retry'), { + ssr: false +}) + +export default function HomePage() { + return ( + loading component}> + + + ) +} diff --git a/e2e/site/pages/suspense-retry-mutate.tsx b/e2e/site/pages/suspense-retry-mutate.tsx new file mode 100644 index 000000000..43b26fd8c --- /dev/null +++ b/e2e/site/pages/suspense-retry-mutate.tsx @@ -0,0 +1,14 @@ +import { Suspense } from 'react' +import dynamic from 'next/dynamic' + +const RemoteData = dynamic(() => import('../component/manual-retry-mutate'), { + ssr: false +}) + +export default function HomePage() { + return ( + loading component}> + + + ) +} diff --git a/e2e/test/initial-render.test.ts b/e2e/test/initial-render.test.ts index ce5b4e879..fa0f430bd 100644 --- a/e2e/test/initial-render.test.ts +++ b/e2e/test/initial-render.test.ts @@ -1,18 +1,32 @@ /* eslint-disable testing-library/prefer-screen-queries */ import { test, expect } from '@playwright/test' -const sleep = async (ms: number) => - new Promise(resolve => setTimeout(resolve, ms)) - test.describe('rendering', () => { - test('should only render once if the result of swr is not used', async ({ + test('suspense with preload', async ({ page }) => { + await page.goto('./suspense-after-preload', { waitUntil: 'commit' }) + await page.getByRole('button', { name: 'preload' }).click() + await expect(page.getByText('suspense-after-preload')).toBeVisible() + }) + test('should be able to retry in suspense with react 18.3', async ({ + page + }) => { + await page.goto('./suspense-retry-18-3', { waitUntil: 'commit' }) + await expect(page.getByText('Something went wrong')).toBeVisible() + await page.getByRole('button', { name: 'retry' }).click() + await expect(page.getByText('data: SWR suspense retry works')).toBeVisible() + }) + test('should be able to retry in suspense with react 18.2', async ({ page }) => { - const log: any[] = [] - await page.exposeFunction('onRender', (msg: any) => log.push(msg)) - await page.goto('./initial-render', { waitUntil: 'commit' }) - await expect(page.getByText('SWRTest')).toBeVisible() - await sleep(1200) - expect(log).toHaveLength(1) + await page.goto('./suspense-retry-18-2', { waitUntil: 'commit' }) + await expect(page.getByText('Something went wrong')).toBeVisible() + await page.getByRole('button', { name: 'retry' }).click() + await expect(page.getByText('data: SWR suspense retry works')).toBeVisible() + }) + test('should be able to retry in suspense with mutate', async ({ page }) => { + await page.goto('./suspense-retry-mutate', { waitUntil: 'commit' }) + await expect(page.getByText('Something went wrong')).toBeVisible() + await page.getByRole('button', { name: 'retry' }).click() + await expect(page.getByText('data: SWR suspense retry works')).toBeVisible() }) }) diff --git a/examples/suspense-retry/app/api/route.ts b/examples/suspense-retry/app/api/route.ts new file mode 100644 index 000000000..1447da32a --- /dev/null +++ b/examples/suspense-retry/app/api/route.ts @@ -0,0 +1,11 @@ +import { NextResponse } from 'next/server' + +export const GET = () => { + return Math.random() < 0.5 + ? NextResponse.json({ + data: 'success' + }) + : new Response('Bad', { + status: 500 + }) +} diff --git a/examples/suspense-retry/app/favicon.ico b/examples/suspense-retry/app/favicon.ico new file mode 100644 index 000000000..4570eb8d9 Binary files /dev/null and b/examples/suspense-retry/app/favicon.ico differ diff --git a/examples/suspense-retry/app/layout.tsx b/examples/suspense-retry/app/layout.tsx new file mode 100644 index 000000000..73a55974b --- /dev/null +++ b/examples/suspense-retry/app/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout({ + children +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/examples/suspense-retry/app/manual-retry.tsx b/examples/suspense-retry/app/manual-retry.tsx new file mode 100644 index 000000000..71e53c8d2 --- /dev/null +++ b/examples/suspense-retry/app/manual-retry.tsx @@ -0,0 +1,44 @@ +'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
{data}
+} +preloadRemote() + +function Fallback({ resetErrorBoundary }: any) { + return ( +
+

Something went wrong:

+ +
+ ) +} + +function RemoteData() { + return ( +
+ { + preloadRemote() + }} + > + loading
}> + + + + + ) +} + +export default RemoteData diff --git a/examples/suspense-retry/app/page.tsx b/examples/suspense-retry/app/page.tsx new file mode 100644 index 000000000..b590f9df8 --- /dev/null +++ b/examples/suspense-retry/app/page.tsx @@ -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 ( + loading component}> + + + ) +} diff --git a/examples/suspense-retry/app/use-remote-data.ts b/examples/suspense-retry/app/use-remote-data.ts new file mode 100644 index 000000000..952927992 --- /dev/null +++ b/examples/suspense-retry/app/use-remote-data.ts @@ -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') + .then(r => r.json()) + .then(r => r.data) +} + +const key = 'manual-retry' + +export const useRemoteData = () => + useSWR(key, fetcher, { + suspense: true + }) + +export const preloadRemote = () => preload(key, fetcher) diff --git a/examples/suspense-retry/next-env.d.ts b/examples/suspense-retry/next-env.d.ts new file mode 100644 index 000000000..fd36f9494 --- /dev/null +++ b/examples/suspense-retry/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/examples/suspense-retry/next.config.js b/examples/suspense-retry/next.config.js new file mode 100644 index 000000000..950e2f42e --- /dev/null +++ b/examples/suspense-retry/next.config.js @@ -0,0 +1,8 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + experimental: { + serverActions: true, + }, +} + +module.exports = nextConfig diff --git a/examples/suspense-retry/package.json b/examples/suspense-retry/package.json new file mode 100644 index 000000000..241b88d99 --- /dev/null +++ b/examples/suspense-retry/package.json @@ -0,0 +1,21 @@ +{ + "name": "site", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@types/node": "^20.2.5", + "@types/react": "^18.2.8", + "@types/react-dom": "18.2.4", + "next": "^13.4.4", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "typescript": "5.1.3", + "swr": "*" + } +} diff --git a/examples/suspense-retry/pages/retry.tsx b/examples/suspense-retry/pages/retry.tsx new file mode 100644 index 000000000..97c01e8c8 --- /dev/null +++ b/examples/suspense-retry/pages/retry.tsx @@ -0,0 +1,14 @@ +import { Suspense } from 'react' +import dynamic from 'next/dynamic' + +const RemoteData = dynamic(() => import('../app/manual-retry'), { + ssr: false +}) + +export default function HomePage() { + return ( + loading component}> + + + ) +} diff --git a/examples/suspense-retry/tsconfig.json b/examples/suspense-retry/tsconfig.json new file mode 100644 index 000000000..ebd7bfca4 --- /dev/null +++ b/examples/suspense-retry/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "baseUrl": ".", + "paths": { + "~/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/package.json b/package.json index caa1383ad..1f8b14f27 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "clean": "pnpm -r run clean && rimraf playwright-report test-result", "watch": "pnpm -r run watch", "build": "pnpm build-package _internal && pnpm build-package core && pnpm build-package infinite && pnpm build-package immutable && pnpm build-package mutation && pnpm build-package subscription", - "build:e2e": "pnpm next build e2e/site -- --profile", + "build:e2e": "pnpm next build e2e/site", "build-package": "bunchee index.ts --cwd", "types:check": "pnpm -r run types:check", "prepublishOnly": "pnpm clean && pnpm build", @@ -126,6 +126,7 @@ "prettier": "2.8.8", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-error-boundary": "^4.0.9", "rimraf": "5.0.1", "semver": "^7.5.1", "swr": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 21a40cc49..73c6ba502 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,6 +89,9 @@ importers: react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + react-error-boundary: + specifier: ^4.0.9 + version: 4.0.9(react@18.2.0) rimraf: specifier: 5.0.1 version: 5.0.1 @@ -4504,6 +4507,15 @@ packages: scheduler: 0.23.0 dev: true + /react-error-boundary@4.0.9(react@18.2.0): + resolution: {integrity: sha512-f6DcHVdTDZmc9ixmRmuLDZpkdghYR/HKZdUzMLHD58s4cR2C4R6y4ktYztCosM6pyeK4/C8IofwqxgID25W6kw==} + peerDependencies: + react: '>=16.13.1' + dependencies: + '@babel/runtime': 7.18.9 + react: 18.2.0 + dev: true + /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} dev: true diff --git a/test/use-swr-suspense.test.tsx b/test/use-swr-suspense.test.tsx index 9487c315a..7aaba6f69 100644 --- a/test/use-swr-suspense.test.tsx +++ b/test/use-swr-suspense.test.tsx @@ -1,7 +1,6 @@ import { act, fireEvent, screen } from '@testing-library/react' -import type { ReactNode, PropsWithChildren } from 'react' import { Profiler } from 'react' -import React, { Suspense, useReducer, useState } from 'react' +import { Suspense, useReducer, useState } from 'react' import useSWR, { mutate } from 'swr' import { createKey, @@ -10,23 +9,7 @@ import { renderWithGlobalCache, sleep } from './utils' - -class ErrorBoundary extends React.Component< - PropsWithChildren<{ fallback: ReactNode }> -> { - state = { hasError: false } - static getDerivedStateFromError() { - return { - hasError: true - } - } - render() { - if (this.state.hasError) { - return this.props.fallback - } - return this.props.children - } -} +import { ErrorBoundary } from 'react-error-boundary' describe('useSWR - suspense', () => { afterEach(() => { diff --git a/tsconfig.json b/tsconfig.json index 22bed18b1..30c150f1c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,7 @@ "target": "ES2018", "baseUrl": ".", "noEmitOnError": true, + "downlevelIteration": true, "paths": { "swr": ["./core/index.ts"], "swr/infinite": ["./infinite/index.ts"],