From c3a125223f0d475e868fa54dfb8a7dc2ace824ea Mon Sep 17 00:00:00 2001 From: Yuta Osawa Date: Mon, 5 May 2025 15:42:59 +0900 Subject: [PATCH 1/2] feat: add SWRGlobalConfig interface for suspense-enabled typing --- package.json | 2 +- src/_internal/types.ts | 11 +++++++++-- src/index/index.ts | 6 ++++++ test/tsconfig.json | 5 +++-- test/type/suspense/helper-types.tsx | 19 +++++++++++++++++++ test/type/suspense/suspense.ts | 13 +++++++++++++ test/type/suspense/tsconfig.json | 10 ++++++++++ test/type/tsconfig.json | 6 ++++-- 8 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 test/type/suspense/helper-types.tsx create mode 100644 test/type/suspense/suspense.ts create mode 100644 test/type/suspense/tsconfig.json diff --git a/package.json b/package.json index 1adf11b6e..fa62eef54 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "lint": "eslint . --ext .ts,.tsx --cache", "lint:fix": "pnpm lint --fix", "coverage": "jest --coverage", - "test-typing": "tsc --noEmit -p test/type/tsconfig.json && tsc --noEmit -p test/tsconfig.json", + "test-typing": "tsc -p test/tsconfig.json && tsc -p test/type/tsconfig.json && tsc -p test/type/suspense/tsconfig.json", "test": "jest", "test:build": "jest --config jest.config.build.js", "test:e2e": "playwright test", diff --git a/src/_internal/types.ts b/src/_internal/types.ts index 528dae932..c3cb0dd22 100644 --- a/src/_internal/types.ts +++ b/src/_internal/types.ts @@ -1,3 +1,4 @@ +import type { SWRGlobalConfig } from '../index' import type * as revalidateEvents from './events' export type GlobalState = [ @@ -33,7 +34,9 @@ export type ReactUsePromise = Promise & { export type BlockingData< Data = any, Options = SWROptions -> = Options extends undefined +> = SWRGlobalConfig extends { suspense: true } + ? true + : Options extends undefined ? false : Options extends { suspense: true } ? true @@ -456,7 +459,11 @@ export type SWRConfiguration< export type IsLoadingResponse< Data = any, Options = SWROptions -> = Options extends { suspense: true } ? false : boolean +> = SWRGlobalConfig extends { suspense: true } + ? Options extends { suspense: true } + ? false + : false + : boolean type SWROptions = SWRConfiguration> type SWRConfigurationWithOptionalFallback = diff --git a/src/index/index.ts b/src/index/index.ts index b5419ddb5..2ae26b143 100644 --- a/src/index/index.ts +++ b/src/index/index.ts @@ -8,6 +8,12 @@ export { useSWRConfig } from '../_internal' export { mutate } from '../_internal' export { preload } from '../_internal' +// Config +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SWRGlobalConfig { + // suspense: true +} + // Types export type { SWRConfiguration, diff --git a/test/tsconfig.json b/test/tsconfig.json index ba0b1f7a8..4cf025629 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.json", "compilerOptions": { + "noEmit": true, "strict": false, "jsx": "react-jsx", "baseUrl": "..", @@ -10,8 +11,8 @@ "swr/immutable": ["./immutable/src/index.ts"], "swr/mutation": ["./mutation/src/index.ts"], "swr/_internal": ["./_internal/src/index.ts"], - "swr/subscription": ["subscription/src/index.ts"], - }, + "swr/subscription": ["subscription/src/index.ts"] + } }, "include": [".", "./jest-setup.ts"], "exclude": ["./type"] diff --git a/test/type/suspense/helper-types.tsx b/test/type/suspense/helper-types.tsx new file mode 100644 index 000000000..0d571f58e --- /dev/null +++ b/test/type/suspense/helper-types.tsx @@ -0,0 +1,19 @@ +import type { BlockingData } from 'swr/_internal' +import { expectType } from '../utils' + +declare module 'swr' { + interface SWRGlobalConfig { + suspense: true + } +} + +export function testDataCached() { + expectType>(true) + expectType>(true) + expectType< + BlockingData + >(true) + expectType>( + true + ) +} diff --git a/test/type/suspense/suspense.ts b/test/type/suspense/suspense.ts new file mode 100644 index 000000000..58323b957 --- /dev/null +++ b/test/type/suspense/suspense.ts @@ -0,0 +1,13 @@ +import useSWR from 'swr' +import { expectType } from '../utils' + +declare module 'swr' { + interface SWRGlobalConfig { + suspense: true + } +} + +export function testSuspense() { + const { data } = useSWR('/api', (k: string) => Promise.resolve(k)) + expectType(data) +} diff --git a/test/type/suspense/tsconfig.json b/test/type/suspense/tsconfig.json new file mode 100644 index 000000000..706abd8cd --- /dev/null +++ b/test/type/suspense/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "noEmit": true, + "strict": true, + "jsx": "react-jsx" + }, + "include": ["./**/*.ts", "./**/*.tsx"], + "exclude": [] +} diff --git a/test/type/tsconfig.json b/test/type/tsconfig.json index 76044134d..ed5b13509 100644 --- a/test/type/tsconfig.json +++ b/test/type/tsconfig.json @@ -1,8 +1,10 @@ { "extends": "../../tsconfig.json", - "compilerOptions": { + "compilerOptions": { + "noEmit": true, "strict": true, - "jsx": "react-jsx", + "jsx": "react-jsx" }, "include": ["./**/*.ts", "./**/*.tsx"], + "exclude": ["./suspense"] } From 25ea8db38308b824259ff12ff088996ff565e764 Mon Sep 17 00:00:00 2001 From: Yuta Osawa Date: Mon, 5 May 2025 15:59:56 +0900 Subject: [PATCH 2/2] examples: add global suspense-enabled example --- examples/suspense-global/README.md | 30 ++++++++++++++ .../components/error-handling.ts | 17 ++++++++ .../suspense-global/global-swr-config.tsx | 24 +++++++++++ examples/suspense-global/libs/fetch.ts | 8 ++++ examples/suspense-global/next-env.d.ts | 5 +++ examples/suspense-global/package.json | 17 ++++++++ .../suspense-global/pages/[user]/[repo].tsx | 30 ++++++++++++++ .../suspense-global/pages/[user]/detail.tsx | 20 ++++++++++ examples/suspense-global/pages/_app.tsx | 10 +++++ examples/suspense-global/pages/api/data.ts | 40 +++++++++++++++++++ examples/suspense-global/pages/index.tsx | 17 ++++++++ examples/suspense-global/pages/repos.tsx | 22 ++++++++++ examples/suspense-global/tsconfig.json | 30 ++++++++++++++ 13 files changed, 270 insertions(+) create mode 100644 examples/suspense-global/README.md create mode 100644 examples/suspense-global/components/error-handling.ts create mode 100644 examples/suspense-global/global-swr-config.tsx create mode 100644 examples/suspense-global/libs/fetch.ts create mode 100644 examples/suspense-global/next-env.d.ts create mode 100644 examples/suspense-global/package.json create mode 100644 examples/suspense-global/pages/[user]/[repo].tsx create mode 100644 examples/suspense-global/pages/[user]/detail.tsx create mode 100644 examples/suspense-global/pages/_app.tsx create mode 100644 examples/suspense-global/pages/api/data.ts create mode 100644 examples/suspense-global/pages/index.tsx create mode 100644 examples/suspense-global/pages/repos.tsx create mode 100644 examples/suspense-global/tsconfig.json diff --git a/examples/suspense-global/README.md b/examples/suspense-global/README.md new file mode 100644 index 000000000..a5f4641f8 --- /dev/null +++ b/examples/suspense-global/README.md @@ -0,0 +1,30 @@ +# Basic + +## One-Click Deploy + +Deploy your own SWR project with Vercel. + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/suspense) + +## How to Use + +Download the example: + +```bash +curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/suspense +cd suspense +``` + +Install it and run: + +```bash +yarn +yarn dev +# or +npm install +npm run dev +``` + +## The Idea behind the Example + +Show how to use the SWR suspense option with React suspense. diff --git a/examples/suspense-global/components/error-handling.ts b/examples/suspense-global/components/error-handling.ts new file mode 100644 index 000000000..f604ae0dd --- /dev/null +++ b/examples/suspense-global/components/error-handling.ts @@ -0,0 +1,17 @@ +import React from 'react' + +export default class ErrorBoundary extends React.Component { + state = { hasError: false, error: null } + static getDerivedStateFromError(error: any) { + return { + hasError: true, + error + } + } + render() { + if (this.state.hasError) { + return this.props.fallback + } + return this.props.children + } +} diff --git a/examples/suspense-global/global-swr-config.tsx b/examples/suspense-global/global-swr-config.tsx new file mode 100644 index 000000000..a8a8f89d2 --- /dev/null +++ b/examples/suspense-global/global-swr-config.tsx @@ -0,0 +1,24 @@ +'use client' + +import { SWRConfig } from 'swr' + +import fetcher from './libs/fetch' + +declare module 'swr' { + interface SWRGlobalConfig { + suspense: true + } +} + +export function GlobalSWRConfig({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} diff --git a/examples/suspense-global/libs/fetch.ts b/examples/suspense-global/libs/fetch.ts new file mode 100644 index 000000000..86f1252fc --- /dev/null +++ b/examples/suspense-global/libs/fetch.ts @@ -0,0 +1,8 @@ +export default async function fetcher(...args: [any]) { + const res = await fetch(...args) + if (!res.ok) { + throw new Error('An error occurred while fetching the data.') + } else { + return res.json() + } +} diff --git a/examples/suspense-global/next-env.d.ts b/examples/suspense-global/next-env.d.ts new file mode 100644 index 000000000..a4a7b3f5c --- /dev/null +++ b/examples/suspense-global/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/suspense-global/package.json b/examples/suspense-global/package.json new file mode 100644 index 000000000..f2671e210 --- /dev/null +++ b/examples/suspense-global/package.json @@ -0,0 +1,17 @@ +{ + "name": "suspense-global", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "dependencies": { + "next": "latest", + "react": "latest", + "react-dom": "latest", + "swr": "latest" + }, + "scripts": { + "dev": "next", + "start": "next start", + "build": "next build" + } +} diff --git a/examples/suspense-global/pages/[user]/[repo].tsx b/examples/suspense-global/pages/[user]/[repo].tsx new file mode 100644 index 000000000..7e2fa7d85 --- /dev/null +++ b/examples/suspense-global/pages/[user]/[repo].tsx @@ -0,0 +1,30 @@ +import dynamic from 'next/dynamic' +import Link from 'next/link' +import { Suspense } from 'react' +import ErrorHandling from '../../components/error-handling' +import { useRouter } from 'next/router' + +const Detail = dynamic(() => import('./detail'), { + ssr: false +}) + +export default function Repo() { + const router = useRouter() + if (!router.isReady) return null + const { user, repo } = router.query + const id = `${user}/${repo}` + + return ( +
+

{id}

+ loading...
}> + oooops!}> + + + +
+
+ Back + + ) +} diff --git a/examples/suspense-global/pages/[user]/detail.tsx b/examples/suspense-global/pages/[user]/detail.tsx new file mode 100644 index 000000000..5201fc2b9 --- /dev/null +++ b/examples/suspense-global/pages/[user]/detail.tsx @@ -0,0 +1,20 @@ +import useSWR from 'swr' +import { RepoData } from '../api/data' + +const Detail = ({ id }: { id: string }) => { + const { data } = useSWR('/api/data?id=' + id) + + return ( + <> + {data ? ( +
+

forks: {data.forks_count}

+

stars: {data.stargazers_count}

+

watchers: {data.watchers}

+
+ ) : null} + + ) +} + +export default Detail diff --git a/examples/suspense-global/pages/_app.tsx b/examples/suspense-global/pages/_app.tsx new file mode 100644 index 000000000..0aae4cb91 --- /dev/null +++ b/examples/suspense-global/pages/_app.tsx @@ -0,0 +1,10 @@ +import type { AppProps } from 'next/app' +import { GlobalSWRConfig } from 'global-swr-config' + +export default function MyApp({ Component, pageProps }: AppProps) { + return ( + + + + ) +} diff --git a/examples/suspense-global/pages/api/data.ts b/examples/suspense-global/pages/api/data.ts new file mode 100644 index 000000000..456a4cc59 --- /dev/null +++ b/examples/suspense-global/pages/api/data.ts @@ -0,0 +1,40 @@ +import { NextApiRequest, NextApiResponse } from 'next' + +const projects = [ + 'facebook/flipper', + 'vuejs/vuepress', + 'rust-lang/rust', + 'vercel/next.js', + 'emperor/clothes' +] as const + +export type ProjectsData = typeof projects + +export interface RepoData { + forks_count: number + stargazers_count: number + watchers: number +} + +export default function api(req: NextApiRequest, res: NextApiResponse) { + if (req.query.id) { + if (req.query.id === projects[4]) { + setTimeout(() => { + res.status(404).json({ msg: 'not found' }) + }) + } else { + // a slow endpoint for getting repo data + fetch(`https://api.github.com/repos/${req.query.id}`) + .then(res => res.json()) + .then(data => { + setTimeout(() => { + res.json(data) + }, 2000) + }) + } + } else { + setTimeout(() => { + res.json(projects) + }, 2000) + } +} diff --git a/examples/suspense-global/pages/index.tsx b/examples/suspense-global/pages/index.tsx new file mode 100644 index 000000000..2da57a688 --- /dev/null +++ b/examples/suspense-global/pages/index.tsx @@ -0,0 +1,17 @@ +import { Suspense } from 'react' +import dynamic from 'next/dynamic' + +const Repos = dynamic(() => import('./repos'), { + ssr: false +}) + +export default function Index() { + return ( +
+

Trending Projects

+ loading...
}> + + + + ) +} diff --git a/examples/suspense-global/pages/repos.tsx b/examples/suspense-global/pages/repos.tsx new file mode 100644 index 000000000..c6a04e121 --- /dev/null +++ b/examples/suspense-global/pages/repos.tsx @@ -0,0 +1,22 @@ +import Link from 'next/link' +import { ProjectsData } from './api/data' + +import useSWR from 'swr' + +const Repos = () => { + const { data } = useSWR('/api/data') + + return ( + <> + {data.map(project => ( +

+ + {project} + +

+ ))} + + ) +} + +export default Repos diff --git a/examples/suspense-global/tsconfig.json b/examples/suspense-global/tsconfig.json new file mode 100644 index 000000000..da520dfb1 --- /dev/null +++ b/examples/suspense-global/tsconfig.json @@ -0,0 +1,30 @@ +{ + "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, + "noImplicitAny": true, + "plugins": [ + { + "name": "next" + } + ], + "baseUrl": ".", + "paths": { + "~/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +}