├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── config.yml │ └── bug_report.md ├── SECURITY.md ├── workflows │ ├── test-legacy-react.yml │ ├── test-canary.yml │ ├── install │ │ └── action.yml │ ├── trigger-release.yml │ └── test-release.yml └── CONTRIBUTING.md ├── .husky └── pre-commit ├── test ├── jest-setup.ts ├── type │ ├── .eslintrc │ ├── utils.ts │ ├── internal.tsx │ ├── suspense │ │ ├── tsconfig.json │ │ ├── suspense.ts │ │ └── helper-types.tsx │ ├── tsconfig.json │ ├── mutation.ts │ ├── helper-types.tsx │ ├── preload.ts │ └── subscription.ts ├── tsconfig.json ├── unit │ └── serialize.test.ts ├── use-swr-devtools.test.tsx ├── use-swr-node-env.test.tsx ├── use-swr-offline.test.tsx ├── use-swr-concurrent-rendering.test.tsx ├── use-swr-legacy-react.test.tsx ├── use-swr-context-config.test.tsx └── use-swr-streaming-ssr.test.tsx ├── src ├── _internal │ ├── constants.ts │ ├── utils │ │ ├── timestamp.ts │ │ ├── middleware-preset.ts │ │ ├── global-state.ts │ │ ├── devtools.ts │ │ ├── use-swr-config.ts │ │ ├── normalize-args.ts │ │ ├── subscribe-key.ts │ │ ├── merge-config.ts │ │ ├── serialize.ts │ │ ├── with-middleware.ts │ │ ├── shared.ts │ │ ├── resolve-args.ts │ │ ├── env.ts │ │ ├── helper.ts │ │ ├── preload.ts │ │ ├── config.ts │ │ ├── web-preset.ts │ │ ├── config-context.ts │ │ └── hash.ts │ ├── events.ts │ ├── index.react-server.ts │ └── index.ts ├── infinite │ ├── index.react-server.ts │ └── serialize.ts ├── index │ ├── index.react-server.ts │ ├── config.ts │ ├── serialize.ts │ └── index.ts ├── immutable │ └── index.ts ├── subscription │ └── types.ts └── mutation │ └── state.ts ├── .editorconfig ├── e2e ├── site │ ├── app │ │ ├── page.tsx │ │ ├── partially-hydrate │ │ │ ├── loading.tsx │ │ │ ├── use-data.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── basic-ssr │ │ │ ├── page.tsx │ │ │ └── block.tsx │ │ ├── issue-2702 │ │ │ ├── page.tsx │ │ │ └── reproduction.tsx │ │ ├── suspense-fallback │ │ │ ├── promise │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── mutate-server-action │ │ │ ├── action.tsx │ │ │ └── page.tsx │ │ ├── suspense-retry │ │ │ ├── page.tsx │ │ │ ├── use-remote-data.ts │ │ │ └── manual-retry.tsx │ │ ├── suspense-after-preload │ │ │ ├── page.tsx │ │ │ └── remote-data.tsx │ │ ├── layout.tsx │ │ ├── react-server-entry │ │ │ └── page.tsx │ │ ├── concurrent-transition │ │ │ ├── page.tsx │ │ │ └── transition-demo.tsx │ │ ├── suspense-undefined-key │ │ │ └── page.tsx │ │ └── perf │ │ │ └── page.tsx │ ├── next.config.js │ ├── pages │ │ ├── api │ │ │ ├── retry.ts │ │ │ └── data.ts │ │ ├── suspense-retry-19.tsx │ │ └── suspense-retry-mutate.tsx │ ├── next-env.d.ts │ ├── lib │ │ └── use-debug-history.ts │ ├── component │ │ ├── use-remote-data.ts │ │ ├── manual-retry.tsx │ │ └── manual-retry-mutate.tsx │ ├── package.json │ ├── tsconfig.json │ └── README.md └── test │ ├── tsconfig.json │ ├── suspense-fallback.test.ts │ ├── mutate-server-action.test.ts │ ├── issue-2702-too-many-hooks.ts │ ├── suspense-undefined-key.test.ts │ ├── concurrent-transition.test.ts │ ├── stream-ssr.test.ts │ └── initial-render.test.ts ├── .npmrc ├── examples ├── suspense │ ├── app │ │ ├── rsc │ │ │ ├── loading.jsx │ │ │ ├── [user] │ │ │ │ └── [repo] │ │ │ │ │ ├── loading.jsx │ │ │ │ │ ├── error.jsx │ │ │ │ │ ├── repo.jsx │ │ │ │ │ └── page.jsx │ │ │ ├── page.jsx │ │ │ └── repos.jsx │ │ └── layout.jsx │ ├── libs │ │ └── fetch.js │ ├── next-env.d.ts │ ├── package.json │ ├── components │ │ └── error-handling.js │ ├── README.md │ └── pages │ │ ├── api │ │ └── data.js │ │ ├── index.js │ │ └── [user] │ │ └── [repo].js ├── local-state-sharing │ ├── libs │ │ └── store.js │ ├── package.json │ ├── README.md │ └── pages │ │ └── index.js ├── .eslintrc ├── basic │ ├── libs │ │ └── fetch.js │ ├── package.json │ ├── pages │ │ ├── api │ │ │ └── data.js │ │ ├── index.js │ │ └── [user] │ │ │ └── [repo].js │ └── README.md ├── infinite │ ├── libs │ │ └── fetch.js │ ├── package.json │ ├── README.md │ └── pages │ │ └── index.js ├── api-hooks │ ├── libs │ │ └── fetch.js │ ├── hooks │ │ ├── use-projects.js │ │ └── use-repository.js │ ├── package.json │ ├── pages │ │ ├── index.js │ │ ├── api │ │ │ └── data.js │ │ └── [user] │ │ │ └── [repo].js │ └── README.md ├── focus-revalidate │ ├── libs │ │ ├── fetch.js │ │ └── auth.js │ ├── package.json │ ├── pages │ │ ├── api │ │ │ └── user.js │ │ └── index.js │ ├── components │ │ └── button.js │ └── README.md ├── global-fetcher │ ├── libs │ │ └── fetch.js │ ├── package.json │ ├── pages │ │ ├── _app.js │ │ ├── api │ │ │ └── data.js │ │ ├── index.js │ │ └── [user] │ │ │ └── [repo].js │ └── README.md ├── infinite-scroll │ ├── libs │ │ └── fetch.js │ ├── package.json │ ├── hooks │ │ └── useOnScreen.js │ ├── README.md │ └── pages │ │ └── index.js ├── prefetch-preload │ ├── libs │ │ └── fetch.js │ ├── package.json │ ├── pages │ │ ├── api │ │ │ └── data.js │ │ ├── [user] │ │ │ └── [repo].js │ │ └── index.js │ └── README.md ├── refetch-interval │ ├── libs │ │ └── fetch.js │ ├── pages │ │ ├── api │ │ │ └── data.js │ │ └── index.js │ ├── package.json │ ├── components │ │ └── button.js │ └── README.md ├── server-render │ ├── libs │ │ └── fetcher.js │ ├── package.json │ ├── pages │ │ ├── index.js │ │ └── [pokemon].js │ └── README.md ├── optimistic-ui-immer │ ├── libs │ │ └── fetch.js │ ├── package.json │ ├── pages │ │ ├── api │ │ │ └── data.js │ │ └── index.js │ └── README.md ├── autocomplete-suggestions │ ├── libs │ │ └── fetcher.js │ ├── package.json │ ├── README.md │ └── pages │ │ └── index.js ├── optimistic-ui │ ├── pages │ │ ├── _app.js │ │ └── api │ │ │ └── todos.js │ ├── libs │ │ └── fetch.js │ ├── package.json │ ├── README.md │ └── styles.css ├── storage-tab-sync │ ├── libs │ │ └── storage.js │ ├── package.json │ ├── pages │ │ └── index.js │ └── README.md ├── suspense-retry │ ├── next.config.js │ ├── app │ │ ├── layout.tsx │ │ ├── api │ │ │ └── route.ts │ │ ├── page.tsx │ │ ├── use-remote-data.ts │ │ └── manual-retry.tsx │ ├── next-env.d.ts │ ├── pages │ │ └── retry.tsx │ ├── package.json │ └── tsconfig.json ├── basic-typescript │ ├── libs │ │ └── fetch.ts │ ├── next-env.d.ts │ ├── package.json │ ├── tsconfig.json │ ├── pages │ │ ├── api │ │ │ └── data.ts │ │ ├── index.tsx │ │ └── [user] │ │ │ └── [repo].tsx │ └── README.md ├── suspense-global │ ├── libs │ │ └── fetch.ts │ ├── next-env.d.ts │ ├── pages │ │ ├── _app.tsx │ │ ├── index.tsx │ │ ├── repos.tsx │ │ ├── [user] │ │ │ ├── detail.tsx │ │ │ └── [repo].tsx │ │ └── api │ │ │ └── data.ts │ ├── package.json │ ├── components │ │ └── error-handling.ts │ ├── global-swr-config.tsx │ ├── README.md │ └── tsconfig.json ├── axios-typescript │ ├── next-env.d.ts │ ├── package.json │ ├── tsconfig.json │ ├── pages │ │ ├── index.tsx │ │ ├── api │ │ │ └── data.ts │ │ └── [user] │ │ │ └── [repo].tsx │ ├── README.md │ └── libs │ │ └── useRequest.ts ├── subscription │ ├── package.json │ ├── README.md │ └── pages │ │ └── index.js └── axios │ ├── package.json │ ├── libs │ └── useRequest.js │ ├── pages │ ├── api │ │ └── data.js │ ├── index.js │ └── [user] │ │ └── [repo].js │ └── README.md ├── pnpm-workspace.yaml ├── jest.config.build.js ├── infinite └── package.json ├── mutation └── package.json ├── _internal └── package.json ├── immutable └── package.json ├── subscription └── package.json ├── .codesandbox └── ci.json ├── .gitignore ├── .swcrc ├── tsconfig.json ├── jest.config.js ├── LICENSE ├── playwright.config.js └── scripts └── bump-next-version.js /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @shuding @huozhi 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm lint-staged && pnpm types:check 2 | -------------------------------------------------------------------------------- /test/jest-setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | -------------------------------------------------------------------------------- /src/_internal/constants.ts: -------------------------------------------------------------------------------- 1 | export const INFINITE_PREFIX = '$inf$' 2 | -------------------------------------------------------------------------------- /src/infinite/index.react-server.ts: -------------------------------------------------------------------------------- 1 | export { unstable_serialize } from './serialize' 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{js,ts,jsx,tsx}] 4 | indent_size = 2 5 | indent_style = space -------------------------------------------------------------------------------- /e2e/site/app/page.tsx: -------------------------------------------------------------------------------- 1 | export default function Home() { 2 | return
SWR E2E Test
3 | } 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # prevent sub-packages from installing peer-deps (multiple react versions) 2 | auto-install-peers=false -------------------------------------------------------------------------------- /e2e/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | ".", 5 | ], 6 | } -------------------------------------------------------------------------------- /examples/suspense/app/rsc/loading.jsx: -------------------------------------------------------------------------------- 1 | export default function Loading() { 2 | return
Loading...
; 3 | } -------------------------------------------------------------------------------- /e2e/site/app/partially-hydrate/loading.tsx: -------------------------------------------------------------------------------- 1 | export default function Loading() { 2 | return
Loading...
3 | } 4 | -------------------------------------------------------------------------------- /examples/local-state-sharing/libs/store.js: -------------------------------------------------------------------------------- 1 | const initialStore = { name: "john" }; 2 | 3 | export default initialStore; 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - '_internal' 3 | - 'core' 4 | - 'immutable' 5 | - 'infinite' 6 | - 'mutation' -------------------------------------------------------------------------------- /examples/suspense/app/rsc/[user]/[repo]/loading.jsx: -------------------------------------------------------------------------------- 1 | export default function Loading() { 2 | return
Loading...
; 3 | } -------------------------------------------------------------------------------- /src/index/index.react-server.ts: -------------------------------------------------------------------------------- 1 | export { unstable_serialize } from './serialize' 2 | export { SWRConfig } from './config' 3 | -------------------------------------------------------------------------------- /test/type/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc", 3 | "rules": { 4 | "react-hooks/rules-of-hooks": 0 5 | } 6 | } -------------------------------------------------------------------------------- /e2e/site/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /examples/.eslintrc: -------------------------------------------------------------------------------- 1 | // next is loading eslintrc from the root directory, adding this to avoid eslint rules being overridden 2 | {} 3 | -------------------------------------------------------------------------------- /src/_internal/utils/timestamp.ts: -------------------------------------------------------------------------------- 1 | // Global timestamp. 2 | let __timestamp = 0 3 | 4 | export const getTimestamp = () => ++__timestamp 5 | -------------------------------------------------------------------------------- /e2e/site/app/basic-ssr/page.tsx: -------------------------------------------------------------------------------- 1 | import Block from './block' 2 | 3 | export default function BasicSSRPage() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /examples/suspense/app/rsc/[user]/[repo]/error.jsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | export default function ErrorPage() { 3 | return
Error happen
; 4 | } -------------------------------------------------------------------------------- /examples/basic/libs/fetch.js: -------------------------------------------------------------------------------- 1 | export default async function fetcher(...args) { 2 | const res = await fetch(...args) 3 | return res.json() 4 | } 5 | -------------------------------------------------------------------------------- /examples/infinite/libs/fetch.js: -------------------------------------------------------------------------------- 1 | export default async function fetcher(...args) { 2 | const res = await fetch(...args) 3 | return res.json() 4 | } 5 | -------------------------------------------------------------------------------- /examples/suspense/libs/fetch.js: -------------------------------------------------------------------------------- 1 | export default async function fetcher(...args) { 2 | const res = await fetch(...args) 3 | return res.json() 4 | } 5 | -------------------------------------------------------------------------------- /examples/api-hooks/libs/fetch.js: -------------------------------------------------------------------------------- 1 | export default async function fetcher(...args) { 2 | const res = await fetch(...args) 3 | return res.json() 4 | } 5 | -------------------------------------------------------------------------------- /examples/focus-revalidate/libs/fetch.js: -------------------------------------------------------------------------------- 1 | export default async function fetcher(...args) { 2 | const res = await fetch(...args) 3 | return res.json() 4 | } 5 | -------------------------------------------------------------------------------- /examples/global-fetcher/libs/fetch.js: -------------------------------------------------------------------------------- 1 | export default async function fetcher(...args) { 2 | const res = await fetch(...args) 3 | return res.json() 4 | } 5 | -------------------------------------------------------------------------------- /examples/infinite-scroll/libs/fetch.js: -------------------------------------------------------------------------------- 1 | export default async function fetcher(...args) { 2 | const res = await fetch(...args) 3 | return res.json() 4 | } 5 | -------------------------------------------------------------------------------- /examples/prefetch-preload/libs/fetch.js: -------------------------------------------------------------------------------- 1 | export default async function fetcher(...args) { 2 | const res = await fetch(...args) 3 | return res.json() 4 | } 5 | -------------------------------------------------------------------------------- /examples/refetch-interval/libs/fetch.js: -------------------------------------------------------------------------------- 1 | export default async function fetcher(...args) { 2 | const res = await fetch(...args) 3 | return res.json() 4 | } 5 | -------------------------------------------------------------------------------- /examples/server-render/libs/fetcher.js: -------------------------------------------------------------------------------- 1 | export default async function fetcher(...args) { 2 | const res = await fetch(...args) 3 | return res.json() 4 | } 5 | -------------------------------------------------------------------------------- /examples/optimistic-ui-immer/libs/fetch.js: -------------------------------------------------------------------------------- 1 | export default async function fetcher(...args) { 2 | const res = await fetch(...args) 3 | return res.json() 4 | } 5 | -------------------------------------------------------------------------------- /examples/autocomplete-suggestions/libs/fetcher.js: -------------------------------------------------------------------------------- 1 | export default async function fetcher(...args) { 2 | const res = await fetch(...args) 3 | return res.json() 4 | } 5 | -------------------------------------------------------------------------------- /src/_internal/events.ts: -------------------------------------------------------------------------------- 1 | export const FOCUS_EVENT = 0 2 | export const RECONNECT_EVENT = 1 3 | export const MUTATE_EVENT = 2 4 | export const ERROR_REVALIDATE_EVENT = 3 5 | -------------------------------------------------------------------------------- /examples/optimistic-ui/pages/_app.js: -------------------------------------------------------------------------------- 1 | import '../styles.css' 2 | 3 | export default function App({ Component, pageProps }) { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /jest.config.build.js: -------------------------------------------------------------------------------- 1 | const config = require("./jest.config"); 2 | module.exports = { 3 | ...config, 4 | // override to use build files 5 | moduleNameMapper: {} 6 | } 7 | -------------------------------------------------------------------------------- /src/_internal/index.react-server.ts: -------------------------------------------------------------------------------- 1 | export { serialize } from './utils/serialize' 2 | export { SWRConfig } from './index' 3 | export { INFINITE_PREFIX } from './constants' 4 | -------------------------------------------------------------------------------- /src/index/config.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | // TODO: fix SWRConfig re-use issue with bundler 4 | import { SWRConfig as S } from '../_internal' 5 | export const SWRConfig = S 6 | -------------------------------------------------------------------------------- /test/type/utils.ts: -------------------------------------------------------------------------------- 1 | export type ExpectType = (value: T) => void 2 | export const expectType: ExpectType = () => {} 3 | 4 | export const truthy: () => boolean = () => true 5 | -------------------------------------------------------------------------------- /infinite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../dist/infinite/index.js", 3 | "module": "../dist/infinite/index.mjs", 4 | "types": "../dist/infinite/index.d.ts", 5 | "private": true 6 | } 7 | -------------------------------------------------------------------------------- /mutation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../dist/mutation/index.js", 3 | "module": "../dist/mutation/index.mjs", 4 | "types": "../dist/mutation/index.d.ts", 5 | "private": true 6 | } 7 | -------------------------------------------------------------------------------- /_internal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../dist/_internal/index.js", 3 | "module": "../dist/_internal/index.mjs", 4 | "types": "../dist/_internal/index.d.ts", 5 | "private": true 6 | } 7 | -------------------------------------------------------------------------------- /immutable/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../dist/immutable/index.js", 3 | "module": "../dist/immutable/index.mjs", 4 | "types": "../dist/immutable/index.d.ts", 5 | "private": true 6 | } 7 | -------------------------------------------------------------------------------- /src/index/serialize.ts: -------------------------------------------------------------------------------- 1 | import type { Key } from '../_internal' 2 | import { serialize } from '../_internal/utils/serialize' 3 | 4 | export const unstable_serialize = (key: Key) => serialize(key)[0] 5 | -------------------------------------------------------------------------------- /e2e/site/app/issue-2702/page.tsx: -------------------------------------------------------------------------------- 1 | import Comp from './reproduction' 2 | 3 | export default function Page() { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /examples/storage-tab-sync/libs/storage.js: -------------------------------------------------------------------------------- 1 | export default async function storage(key) { 2 | const value = localStorage.getItem(key) 3 | if (!value) return undefined 4 | return JSON.parse(value) 5 | } 6 | -------------------------------------------------------------------------------- /subscription/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../dist/subscription/index.js", 3 | "module": "../dist/subscription/index.mjs", 4 | "types": "../dist/subscription/index.d.ts", 5 | "private": true 6 | } 7 | -------------------------------------------------------------------------------- /examples/api-hooks/hooks/use-projects.js: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr' 2 | 3 | import fetch from '../libs/fetch' 4 | 5 | export default function useProjects() { 6 | return useSWR('/api/data', fetch) 7 | } 8 | 9 | -------------------------------------------------------------------------------- /examples/optimistic-ui/libs/fetch.js: -------------------------------------------------------------------------------- 1 | export default async function fetcher(...args) { 2 | const res = await fetch(...args) 3 | if (!res.ok) throw new Error('Failed to fetch') 4 | return res.json() 5 | } 6 | -------------------------------------------------------------------------------- /examples/suspense-retry/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | serverActions: true, 5 | }, 6 | } 7 | 8 | module.exports = nextConfig 9 | -------------------------------------------------------------------------------- /src/_internal/utils/middleware-preset.ts: -------------------------------------------------------------------------------- 1 | import { use as devtoolsUse } from './devtools' 2 | import { middleware as preload } from './preload' 3 | 4 | export const BUILT_IN_MIDDLEWARE = devtoolsUse.concat(preload) 5 | -------------------------------------------------------------------------------- /examples/api-hooks/hooks/use-repository.js: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr' 2 | 3 | import fetch from '../libs/fetch' 4 | 5 | export default function useRepository(id) { 6 | return useSWR('/api/data?id=' + id, fetch) 7 | } 8 | -------------------------------------------------------------------------------- /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "sandboxes": ["swr-basic-p7dg6", "swr-states-4une7", "swr-infinite-jb5bm", "swr-ssr-j9b2y"], 3 | "node": "18", 4 | "installCommand": "csb:install", 5 | "buildCommand": "csb:build" 6 | } 7 | -------------------------------------------------------------------------------- /src/_internal/utils/global-state.ts: -------------------------------------------------------------------------------- 1 | import type { Cache, GlobalState } from '../types' 2 | 3 | // Global state used to deduplicate requests and store listeners 4 | export const SWRGlobalState = new WeakMap() 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask Question 4 | url: https://github.com/vercel/swr/discussions 5 | about: Ask questions and discuss with other community members 6 | -------------------------------------------------------------------------------- /examples/basic-typescript/libs/fetch.ts: -------------------------------------------------------------------------------- 1 | export default async function fetcher( 2 | input: RequestInfo, 3 | init?: RequestInit 4 | ): Promise { 5 | const res = await fetch(input, init) 6 | return res.json() 7 | } 8 | -------------------------------------------------------------------------------- /examples/suspense/app/layout.jsx: -------------------------------------------------------------------------------- 1 | export default function RootLayout({ 2 | children 3 | }) { 4 | return ( 5 | 6 | {children} 7 | 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /examples/suspense-retry/app/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function RootLayout({ 2 | children 3 | }: { 4 | children: React.ReactNode 5 | }) { 6 | return ( 7 | 8 | {children} 9 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /test/type/internal.tsx: -------------------------------------------------------------------------------- 1 | import { rAF } from 'swr/_internal' 2 | import { expectType } from './utils' 3 | 4 | export function rAFTyping() { 5 | expectType< 6 | (f: (...args: any[]) => void) => ReturnType | number 7 | >(rAF) 8 | } 9 | -------------------------------------------------------------------------------- /test/type/suspense/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "strict": true, 6 | "jsx": "react-jsx" 7 | }, 8 | "include": ["./**/*.ts", "./**/*.tsx"], 9 | "exclude": [] 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.log 4 | *.tgz 5 | .env 6 | .next 7 | .DS_Store 8 | .idea 9 | .vscode 10 | .eslintcache 11 | examples/**/yarn.lock 12 | package-lock.json 13 | *.tsbuildinfo 14 | coverage 15 | .rollup.cache 16 | playwright-report 17 | test-results -------------------------------------------------------------------------------- /test/type/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "strict": true, 6 | "jsx": "react-jsx" 7 | }, 8 | "include": ["./**/*.ts", "./**/*.tsx"], 9 | "exclude": ["./suspense"] 10 | } 11 | -------------------------------------------------------------------------------- /e2e/site/app/partially-hydrate/use-data.tsx: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr' 2 | 3 | export default function useData() { 4 | return useSWR('/api/data', async (url: string) => { 5 | const res = await fetch(url).then(v => v.json()) 6 | return res.name 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /e2e/site/app/suspense-fallback/promise/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import useSWR from 'swr' 4 | 5 | export default function Page() { 6 | const { data, isLoading } = useSWR('/api/promise') 7 | 8 | return
{isLoading ? 'loading...' : data?.value}
9 | } 10 | -------------------------------------------------------------------------------- /examples/suspense-global/libs/fetch.ts: -------------------------------------------------------------------------------- 1 | export default async function fetcher(...args: [any]) { 2 | const res = await fetch(...args) 3 | if (!res.ok) { 4 | throw new Error('An error occurred while fetching the data.') 5 | } else { 6 | return res.json() 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/suspense/app/rsc/page.jsx: -------------------------------------------------------------------------------- 1 | import fetcher from '../../libs/fetch' 2 | import Repos from './repos' 3 | const Page = () => { 4 | const serverData = fetcher('http://localhost:3000/api/data') 5 | return 6 | } 7 | 8 | export default Page 9 | -------------------------------------------------------------------------------- /examples/focus-revalidate/libs/auth.js: -------------------------------------------------------------------------------- 1 | // mock login and logout 2 | 3 | export function login() { 4 | document.cookie = 'swr-test-token=swr;' 5 | } 6 | 7 | export function logout() { 8 | document.cookie = 'swr-test-token=; expires=Thu, 01 Jan 1970 00:00:01 GMT;' 9 | } 10 | -------------------------------------------------------------------------------- /examples/suspense-global/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/axios-typescript/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/basic-features/typescript for more information. 7 | -------------------------------------------------------------------------------- /examples/basic-typescript/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/basic-features/typescript for more information. 7 | -------------------------------------------------------------------------------- /examples/suspense-retry/app/api/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server' 2 | 3 | export const GET = () => { 4 | return Math.random() < 0.5 5 | ? NextResponse.json({ 6 | data: 'success' 7 | }) 8 | : new Response('Bad', { 9 | status: 500 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /examples/refetch-interval/pages/api/data.js: -------------------------------------------------------------------------------- 1 | // an simple endpoint for getting current list 2 | let list = [] 3 | 4 | export default function api(req, res) { 5 | if (req.query.add) { 6 | list.push(req.query.add) 7 | } else if (req.query.clear) { 8 | list = [] 9 | } 10 | res.json(list) 11 | } 12 | -------------------------------------------------------------------------------- /examples/suspense/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/basic-features/typescript for more information. 7 | -------------------------------------------------------------------------------- /test/type/mutation.ts: -------------------------------------------------------------------------------- 1 | import useSWRMutation, { type TriggerWithoutArgs } from 'swr/mutation' 2 | import { expectType } from './utils' 3 | 4 | export function useConfigMutation() { 5 | const { trigger } = useSWRMutation('key', k => k) 6 | expectType>(trigger) 7 | } 8 | -------------------------------------------------------------------------------- /.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/swcrc", 3 | "sourceMaps": true, 4 | "jsc": { 5 | "parser": { 6 | "syntax": "typescript", 7 | "tsx": true 8 | }, 9 | "transform": { 10 | "react": { 11 | "runtime": "automatic" 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/suspense-retry/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/basic-features/typescript for more information. 7 | -------------------------------------------------------------------------------- /e2e/site/pages/api/retry.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | 3 | type Data = { 4 | name: string 5 | } 6 | 7 | export default function handler( 8 | req: NextApiRequest, 9 | res: NextApiResponse 10 | ) { 11 | res.status(200).json({ name: 'SWR suspense retry works' }) 12 | } 13 | -------------------------------------------------------------------------------- /examples/suspense-global/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app' 2 | import { GlobalSWRConfig } from 'global-swr-config' 3 | 4 | export default function MyApp({ Component, pageProps }: AppProps) { 5 | return ( 6 | 7 | 8 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /e2e/site/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | import './.next/types/routes.d.ts' 5 | 6 | // NOTE: This file should not be edited 7 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 8 | -------------------------------------------------------------------------------- /test/type/suspense/suspense.ts: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr' 2 | import { expectType } from '../utils' 3 | 4 | declare module 'swr' { 5 | interface SWRGlobalConfig { 6 | suspense: true 7 | } 8 | } 9 | 10 | export function useTestSuspense() { 11 | const { data } = useSWR('/api', (k: string) => Promise.resolve(k)) 12 | expectType(data) 13 | } 14 | -------------------------------------------------------------------------------- /e2e/site/app/mutate-server-action/action.tsx: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | export async function action(): Promise<{ result: number }> { 4 | await sleep(500) 5 | return { result: 10086 } 6 | } 7 | 8 | function sleep(ms: number): Promise { 9 | return new Promise(resolve => { 10 | setTimeout(() => { 11 | resolve() 12 | }, ms) 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "private": true, 4 | "license": "MIT", 5 | "dependencies": { 6 | "next": "latest", 7 | "react": "latest", 8 | "react-dom": "latest", 9 | "swr": "latest" 10 | }, 11 | "scripts": { 12 | "dev": "next", 13 | "start": "next start", 14 | "build": "next build" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | If you believe you have found a security vulnerability in SWR, we encourage you to let us know right away. 4 | 5 | We will investigate all legitimate reports and do our best to quickly fix the problem. 6 | 7 | Email `security@vercel.com` to disclose any security vulnerabilities. 8 | 9 | https://vercel.com/security 10 | -------------------------------------------------------------------------------- /examples/subscription/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "subscription", 3 | "private": true, 4 | "license": "MIT", 5 | "dependencies": { 6 | "next": "latest", 7 | "react": "latest", 8 | "react-dom": "latest", 9 | "swr": "latest" 10 | }, 11 | "scripts": { 12 | "dev": "next", 13 | "start": "next start", 14 | "build": "next build" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /e2e/site/pages/api/data.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'SSR Works' }) 13 | } 14 | -------------------------------------------------------------------------------- /examples/suspense-retry/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react' 2 | import dynamic from 'next/dynamic' 3 | 4 | const RemoteData = dynamic(() => import('./manual-retry'), { 5 | ssr: false 6 | }) 7 | 8 | export default function HomePage() { 9 | return ( 10 | loading component}> 11 | 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /e2e/test/suspense-fallback.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test' 2 | 3 | test.describe('suspense fallback', () => { 4 | test('should wait for promise fallback value to be resolved', async ({ 5 | page 6 | }) => { 7 | await page.goto('./suspense-fallback/promise', { waitUntil: 'commit' }) 8 | await expect(page.getByText('async promise')).toBeVisible() 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /examples/api-hooks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-hooks", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "next": "latest", 8 | "react": "latest", 9 | "react-dom": "latest", 10 | "swr": "latest" 11 | }, 12 | "scripts": { 13 | "dev": "next", 14 | "start": "next start", 15 | "build": "next build" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/infinite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "infinite", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "next": "latest", 8 | "react": "latest", 9 | "react-dom": "latest", 10 | "swr": "latest" 11 | }, 12 | "scripts": { 13 | "dev": "next", 14 | "start": "next start", 15 | "build": "next build" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/suspense/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "suspense", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "next": "latest", 8 | "react": "latest", 9 | "react-dom": "latest", 10 | "swr": "latest" 11 | }, 12 | "scripts": { 13 | "dev": "next", 14 | "start": "next start", 15 | "build": "next build" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /e2e/site/pages/suspense-retry-19.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react' 2 | import dynamic from 'next/dynamic' 3 | 4 | const RemoteData = dynamic(() => import('../component/manual-retry'), { 5 | ssr: false 6 | }) 7 | 8 | export default function HomePage() { 9 | return ( 10 | loading component}> 11 | 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /examples/suspense-retry/pages/retry.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react' 2 | import dynamic from 'next/dynamic' 3 | 4 | const RemoteData = dynamic(() => import('../app/manual-retry'), { 5 | ssr: false 6 | }) 7 | 8 | export default function HomePage() { 9 | return ( 10 | loading component}> 11 | 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /examples/global-fetcher/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "global-fetcher", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "next": "latest", 8 | "react": "latest", 9 | "react-dom": "latest", 10 | "swr": "latest" 11 | }, 12 | "scripts": { 13 | "dev": "next", 14 | "start": "next start", 15 | "build": "next build" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/infinite-scroll/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "infinite-scroll", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "next": "latest", 8 | "react": "latest", 9 | "react-dom": "latest", 10 | "swr": "latest" 11 | }, 12 | "scripts": { 13 | "dev": "next", 14 | "start": "next start", 15 | "build": "next build" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/optimistic-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "optimistic-ui", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "next": "latest", 8 | "react": "latest", 9 | "react-dom": "latest", 10 | "swr": "latest" 11 | }, 12 | "scripts": { 13 | "dev": "next", 14 | "start": "next start", 15 | "build": "next build" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/server-render/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server-render", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "next": "latest", 8 | "react": "latest", 9 | "react-dom": "latest", 10 | "swr": "latest" 11 | }, 12 | "scripts": { 13 | "dev": "next", 14 | "start": "next start", 15 | "build": "next build" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/suspense-global/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "suspense-global", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "next": "latest", 8 | "react": "latest", 9 | "react-dom": "latest", 10 | "swr": "latest" 11 | }, 12 | "scripts": { 13 | "dev": "next", 14 | "start": "next start", 15 | "build": "next build" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /e2e/site/pages/suspense-retry-mutate.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react' 2 | import dynamic from 'next/dynamic' 3 | 4 | const RemoteData = dynamic(() => import('../component/manual-retry-mutate'), { 5 | ssr: false 6 | }) 7 | 8 | export default function HomePage() { 9 | return ( 10 | loading component}> 11 | 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /examples/focus-revalidate/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "focus-revalidate", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "next": "latest", 8 | "react": "latest", 9 | "react-dom": "latest", 10 | "swr": "latest" 11 | }, 12 | "scripts": { 13 | "dev": "next", 14 | "start": "next start", 15 | "build": "next build" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/prefetch-preload/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prefetch-preload", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "next": "latest", 8 | "react": "latest", 9 | "react-dom": "latest", 10 | "swr": "latest" 11 | }, 12 | "scripts": { 13 | "dev": "next", 14 | "start": "next start", 15 | "build": "next build" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/refetch-interval/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "refetch-interval", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "next": "latest", 8 | "react": "latest", 9 | "react-dom": "latest", 10 | "swr": "latest" 11 | }, 12 | "scripts": { 13 | "dev": "next", 14 | "start": "next start", 15 | "build": "next build" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/storage-tab-sync/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storage-tab-sync", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "next": "latest", 8 | "react": "latest", 9 | "react-dom": "latest", 10 | "swr": "latest" 11 | }, 12 | "scripts": { 13 | "dev": "next", 14 | "start": "next start", 15 | "build": "next build" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /e2e/site/app/suspense-retry/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Suspense } from 'react' 4 | import dynamic from 'next/dynamic' 5 | 6 | const RemoteData = dynamic(() => import('./manual-retry'), { 7 | ssr: false 8 | }) 9 | 10 | export default function HomePage() { 11 | return ( 12 | loading component}> 13 | 14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /examples/axios/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "axios": "0.27.2", 8 | "next": "latest", 9 | "react": "latest", 10 | "react-dom": "latest", 11 | "swr": "latest" 12 | }, 13 | "scripts": { 14 | "dev": "next", 15 | "start": "next start", 16 | "build": "next build" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/focus-revalidate/pages/api/user.js: -------------------------------------------------------------------------------- 1 | // an endpoint for getting user info 2 | export default function user(req, res) { 3 | if (req.cookies['swr-test-token'] === 'swr') { 4 | // authorized 5 | res.json({ 6 | loggedIn: true, 7 | name: 'Shu', 8 | avatar: 'https://github.com/shuding.png' 9 | }) 10 | return 11 | } 12 | 13 | res.json({ 14 | loggedIn: false 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /examples/local-state-sharing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "local-state-sharing", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "react": "latest", 8 | "react-dom": "latest", 9 | "next": "latest", 10 | "swr": "latest" 11 | }, 12 | "scripts": { 13 | "dev": "next", 14 | "start": "next start", 15 | "build": "next build" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /e2e/site/app/suspense-after-preload/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Suspense } from 'react' 4 | import dynamic from 'next/dynamic' 5 | 6 | const RemoteData = dynamic(() => import('./remote-data'), { 7 | ssr: false 8 | }) 9 | 10 | export default function HomePage() { 11 | return ( 12 | loading component}> 13 | 14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /examples/optimistic-ui-immer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "optimistic-ui-immer", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "immer": "9.0.5", 8 | "next": "latest", 9 | "react": "latest", 10 | "react-dom": "latest", 11 | "swr": "latest" 12 | }, 13 | "scripts": { 14 | "dev": "next", 15 | "start": "next start", 16 | "build": "next build" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/focus-revalidate/components/button.js: -------------------------------------------------------------------------------- 1 | export default function Button({ children, ...props }) { 2 | return
16 | } -------------------------------------------------------------------------------- /examples/refetch-interval/components/button.js: -------------------------------------------------------------------------------- 1 | export default function Button({ children, ...props }) { 2 | return
16 | } -------------------------------------------------------------------------------- /e2e/site/app/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function RootLayout({ 2 | children 3 | }: { 4 | children: React.ReactNode 5 | }) { 6 | return ( 7 | 8 | {/* 9 | will contain the components returned by the nearest parent 10 | head.tsx. Find out more at https://beta.nextjs.org/docs/api-reference/file-conventions/head 11 | */} 12 | 13 | {children} 14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /examples/global-fetcher/pages/_app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import App from 'next/app' 3 | import { SWRConfig } from 'swr' 4 | import fetch from '../libs/fetch.js'; 5 | 6 | export default class MyApp extends App { 7 | render() { 8 | const { Component, pageProps } = this.props 9 | return 14 | 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/suspense/components/error-handling.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default class ErrorBoundary extends React.Component { 4 | state = { hasError: false, error: null } 5 | static getDerivedStateFromError(error) { 6 | return { 7 | hasError: true, 8 | error 9 | } 10 | } 11 | render() { 12 | if (this.state.hasError) { 13 | return this.props.fallback 14 | } 15 | return this.props.children 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/suspense-global/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react' 2 | import dynamic from 'next/dynamic' 3 | 4 | const Repos = dynamic(() => import('./repos'), { 5 | ssr: false 6 | }) 7 | 8 | export default function Index() { 9 | return ( 10 |
11 |

Trending Projects

12 | loading...
}> 13 | 14 | 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/infinite/serialize.ts: -------------------------------------------------------------------------------- 1 | import type { SWRInfiniteKeyLoader } from './types' 2 | import { serialize } from '../_internal/utils/serialize' 3 | import { INFINITE_PREFIX } from '../_internal/constants' 4 | 5 | export const getFirstPageKey = (getKey: SWRInfiniteKeyLoader) => { 6 | return serialize(getKey ? getKey(0, null) : null)[0] 7 | } 8 | 9 | export const unstable_serialize = (getKey: SWRInfiniteKeyLoader) => { 10 | return INFINITE_PREFIX + getFirstPageKey(getKey) 11 | } 12 | -------------------------------------------------------------------------------- /examples/suspense-global/components/error-handling.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default class ErrorBoundary extends React.Component { 4 | state = { hasError: false, error: null } 5 | static getDerivedStateFromError(error: any) { 6 | return { 7 | hasError: true, 8 | error 9 | } 10 | } 11 | render() { 12 | if (this.state.hasError) { 13 | return this.props.fallback 14 | } 15 | return this.props.children 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /e2e/site/lib/use-debug-history.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useEffect, useRef } from 'react' 3 | 4 | export const useDebugHistory = (data: T, prefix = '') => { 5 | const dataRef = useRef([]) 6 | const debugRef = useRef(null) 7 | useEffect(() => { 8 | dataRef.current.push(data) 9 | if (debugRef.current) { 10 | debugRef.current.innerText = `${prefix}${JSON.stringify(dataRef.current)}` 11 | } 12 | }, [data, prefix]) 13 | return debugRef 14 | } 15 | -------------------------------------------------------------------------------- /src/_internal/utils/devtools.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { isWindowDefined } from './helper' 3 | 4 | // @ts-expect-error 5 | const enableDevtools = isWindowDefined && window.__SWR_DEVTOOLS_USE__ 6 | 7 | export const use = enableDevtools 8 | ? // @ts-expect-error 9 | window.__SWR_DEVTOOLS_USE__ 10 | : [] 11 | 12 | export const setupDevTools = () => { 13 | if (enableDevtools) { 14 | // @ts-expect-error 15 | window.__SWR_DEVTOOLS_REACT__ = React 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/autocomplete-suggestions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "autocomplete-suggestions", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "@reach/combobox": "0.16.1", 8 | "lodash.debounce": "4.0.8", 9 | "next": "latest", 10 | "react": "latest", 11 | "react-dom": "latest", 12 | "swr": "latest" 13 | }, 14 | "scripts": { 15 | "dev": "next", 16 | "start": "next start", 17 | "build": "next build" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/type/helper-types.tsx: -------------------------------------------------------------------------------- 1 | import type { BlockingData } from 'swr/_internal' 2 | import { expectType } from './utils' 3 | 4 | export function testDataCached() { 5 | expectType>(true) 6 | expectType>(true) 7 | expectType< 8 | BlockingData 9 | >(false) 10 | expectType>( 11 | false 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /examples/axios/libs/useRequest.js: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr' 2 | import axios from 'axios' 3 | 4 | export default function useRequest(request, { fallbackData, ...config } = {}) { 5 | return useSWR( 6 | request, 7 | () => axios(request || {}).then(response => response.data), 8 | { 9 | ...config, 10 | fallbackData: fallbackData && { 11 | status: 200, 12 | statusText: 'InitialData', 13 | headers: {}, 14 | data: fallbackData 15 | } 16 | } 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /examples/suspense-global/global-swr-config.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { SWRConfig } from 'swr' 4 | 5 | import fetcher from './libs/fetch' 6 | 7 | declare module 'swr' { 8 | interface SWRGlobalConfig { 9 | suspense: true 10 | } 11 | } 12 | 13 | export function GlobalSWRConfig({ children }: { children: React.ReactNode }) { 14 | return ( 15 | 21 | {children} 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /e2e/site/app/react-server-entry/page.tsx: -------------------------------------------------------------------------------- 1 | import { unstable_serialize } from 'swr' 2 | import { unstable_serialize as infinite_unstable_serialize } from 'swr/infinite' 3 | 4 | export default function Page() { 5 | return ( 6 | <> 7 |
SWR Server Component entry test
8 |
unstable_serialize: {unstable_serialize('useSWR')}
9 |
10 | infinite_unstable_serialize:{' '} 11 | {infinite_unstable_serialize(() => 'useSWRInfinite')} 12 |
13 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /e2e/site/component/use-remote-data.ts: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr' 2 | import { preload } from 'swr' 3 | 4 | let count = 0 5 | export const fetcher = () => { 6 | count++ 7 | if (count === 1) return Promise.reject('wrong') 8 | return fetch('/api/retry') 9 | .then(r => r.json()) 10 | .then(r => r.name) 11 | } 12 | 13 | const key = 'manual-retry-18-2' 14 | 15 | export const useRemoteData = () => 16 | useSWR(key, fetcher, { 17 | suspense: true 18 | }) 19 | 20 | export const preloadRemote = () => preload(key, fetcher) 21 | -------------------------------------------------------------------------------- /examples/suspense-global/pages/repos.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import { ProjectsData } from './api/data' 3 | 4 | import useSWR from 'swr' 5 | 6 | const Repos = () => { 7 | const { data } = useSWR('/api/data') 8 | 9 | return ( 10 | <> 11 | {data.map(project => ( 12 |

13 | 14 | {project} 15 | 16 |

17 | ))} 18 | 19 | ) 20 | } 21 | 22 | export default Repos 23 | -------------------------------------------------------------------------------- /e2e/site/app/suspense-fallback/layout.tsx: -------------------------------------------------------------------------------- 1 | import { SWRConfig } from 'swr' 2 | 3 | function createPromiseData(data: any, timeout: number) { 4 | return new Promise(resolve => { 5 | setTimeout(() => { 6 | resolve(data) 7 | }, timeout) 8 | }) 9 | } 10 | 11 | export default function Layout({ children }: { children: React.ReactNode }) { 12 | const fallback = { 13 | '/api/promise': createPromiseData({ value: 'async promise' }, 2000) 14 | } 15 | 16 | return {children} 17 | } 18 | -------------------------------------------------------------------------------- /e2e/site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "site", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@types/node": "^22.19.1", 13 | "@types/react": "^19.2.7", 14 | "@types/react-dom": "^19.2.7", 15 | "next": "^16.0.10", 16 | "react": "^19.1.4", 17 | "react-dom": "^19.1.4", 18 | "typescript": "5.9.3", 19 | "swr": "link:../../" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/optimistic-ui-immer/pages/api/data.js: -------------------------------------------------------------------------------- 1 | const data = [] 2 | 3 | function shouldFail() { 4 | return Math.random() > 0.8 5 | } 6 | 7 | export default function api(req, res) { 8 | if (req.method === 'POST') { 9 | const body = JSON.parse(req.body) 10 | // sometimes it will fail, and this will cause a regression in the UI 11 | if (!shouldFail()) { 12 | data.push(body.text); 13 | } 14 | res.json(data) 15 | return 16 | } 17 | 18 | setTimeout(() => { 19 | res.json(data) 20 | }, 2000) 21 | } 22 | 23 | -------------------------------------------------------------------------------- /examples/suspense-retry/app/use-remote-data.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import useSWR from 'swr' 3 | import { preload } from 'swr' 4 | 5 | let count = 0 6 | const fetcher = () => { 7 | count++ 8 | if (count === 1) return Promise.reject('wrong') 9 | return fetch('/api') 10 | .then(r => r.json()) 11 | .then(r => r.data) 12 | } 13 | 14 | const key = 'manual-retry' 15 | 16 | export const useRemoteData = () => 17 | useSWR(key, fetcher, { 18 | suspense: true 19 | }) 20 | 21 | export const preloadRemote = () => preload(key, fetcher) 22 | -------------------------------------------------------------------------------- /examples/suspense-retry/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "site", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@types/node": "^20.2.5", 13 | "@types/react": "^18.2.8", 14 | "@types/react-dom": "18.2.4", 15 | "next": "^latest", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0", 18 | "typescript": "5.1.3", 19 | "swr": "*" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/suspense-global/pages/[user]/detail.tsx: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr' 2 | import { RepoData } from '../api/data' 3 | 4 | const Detail = ({ id }: { id: string }) => { 5 | const { data } = useSWR('/api/data?id=' + id) 6 | 7 | return ( 8 | <> 9 | {data ? ( 10 |
11 |

forks: {data.forks_count}

12 |

stars: {data.stargazers_count}

13 |

watchers: {data.watchers}

14 |
15 | ) : null} 16 | 17 | ) 18 | } 19 | 20 | export default Detail 21 | -------------------------------------------------------------------------------- /e2e/site/app/basic-ssr/block.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import useSWR from 'swr' 4 | import { useDebugHistory } from '~/lib/use-debug-history' 5 | 6 | export default function Block() { 7 | const { data } = useSWR('/api/data', async (url: string) => { 8 | const res = await fetch(url).then(v => v.json()) 9 | return res.name 10 | }) 11 | const debugRef = useDebugHistory(data, 'history:') 12 | return ( 13 | <> 14 |
15 |
result:{data || 'undefined'}
16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /examples/basic-typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-typescript", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "next": "latest", 8 | "react": "latest", 9 | "react-dom": "latest", 10 | "swr": "latest" 11 | }, 12 | "scripts": { 13 | "dev": "next", 14 | "start": "next start", 15 | "build": "next build" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "16.7.2", 19 | "@types/react": "17.0.19", 20 | "typescript": "4.3.5" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /e2e/site/app/suspense-retry/use-remote-data.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import useSWR from 'swr' 3 | import { preload } from 'swr' 4 | 5 | let count = 0 6 | const fetcher = () => { 7 | count++ 8 | if (count === 1) return Promise.reject('wrong') 9 | return fetch('/api/retry') 10 | .then(r => r.json()) 11 | .then(r => r.name) 12 | } 13 | 14 | const key = 'manual-retry-18-3' 15 | 16 | export const useRemoteData = () => 17 | useSWR(key, fetcher, { 18 | suspense: true 19 | }) 20 | 21 | export const preloadRemote = () => preload(key, fetcher) 22 | -------------------------------------------------------------------------------- /examples/api-hooks/pages/index.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import useProjects from '../hooks/use-projects' 3 | 4 | export default function Index() { 5 | const { data } = useProjects() 6 | 7 | return ( 8 |
9 |

Trending Projects

10 |
11 | { 12 | data ? data.map(project => 13 |

{project}

14 | ) : 'loading...' 15 | } 16 |
17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/_internal/utils/use-swr-config.ts: -------------------------------------------------------------------------------- 1 | import { useContext, useMemo } from 'react' 2 | import { defaultConfig } from './config' 3 | import { SWRConfigContext } from './config-context' 4 | import { mergeObjects } from './shared' 5 | import type { FullConfiguration } from '../types' 6 | 7 | export const useSWRConfig = (): FullConfiguration => { 8 | const parentConfig = useContext(SWRConfigContext) 9 | const mergedConfig = useMemo( 10 | () => mergeObjects(defaultConfig, parentConfig), 11 | [parentConfig] 12 | ) 13 | return mergedConfig 14 | } 15 | -------------------------------------------------------------------------------- /e2e/test/mutate-server-action.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test' 2 | 3 | test('mutate-server-action', async ({ page }) => { 4 | await page.goto('./mutate-server-action') 5 | await page.getByRole('button', { name: 'mutate' }).click() 6 | await expect(page.getByText('isMutating: true')).toBeVisible() 7 | await expect(page.getByText('data: ')).toBeVisible() 8 | await page.waitForTimeout(500) 9 | await expect(page.getByText('isMutating: false')).toBeVisible() 10 | await expect(page.getByText('data: 10086')).toBeVisible() 11 | }) 12 | -------------------------------------------------------------------------------- /src/immutable/index.ts: -------------------------------------------------------------------------------- 1 | import type { Middleware } from '../index' 2 | import useSWR from '../index' 3 | import { withMiddleware } from '../_internal' 4 | 5 | export const immutable: Middleware = useSWRNext => (key, fetcher, config) => { 6 | // Always override all revalidate options. 7 | config.revalidateOnFocus = false 8 | config.revalidateIfStale = false 9 | config.revalidateOnReconnect = false 10 | return useSWRNext(key, fetcher, config) 11 | } 12 | 13 | const useSWRImmutable = withMiddleware(useSWR, immutable) 14 | 15 | export default useSWRImmutable 16 | -------------------------------------------------------------------------------- /e2e/test/issue-2702-too-many-hooks.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test' 2 | 3 | test.describe('issue 2702', () => { 4 | test('should not crash with too many hooks', async ({ page }) => { 5 | // Navigate to the test page 6 | await page.goto('./issue-2702', { waitUntil: 'networkidle' }) 7 | 8 | // Wait for the page to be fully loaded and interactive 9 | await expect(page.getByText('fetching')).toBeVisible() 10 | 11 | // Verify that the component renders correctly 12 | await expect(page.getByText('a,b')).toBeVisible() 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /examples/axios-typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-typescript", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "axios": "0.23.0", 8 | "next": "latest", 9 | "react": "latest", 10 | "react-dom": "latest", 11 | "swr": "latest" 12 | }, 13 | "scripts": { 14 | "dev": "next", 15 | "start": "next start", 16 | "build": "next build" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "16.7.2", 20 | "@types/react": "17.0.19", 21 | "typescript": "4.3.5" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report for the SWR library 4 | --- 5 | 6 | # Bug report 7 | 8 | ## Description / Observed Behavior 9 | 10 | What kind of issues did you encounter with SWR? 11 | 12 | ## Expected Behavior 13 | 14 | How did you expect SWR to behave here? 15 | 16 | ## Repro Steps / Code Example 17 | 18 | Or share your code snippet or a [CodeSandbox](https://codesandbox.io) link is also appreciated! 19 | 20 | ## Additional Context 21 | 22 | SWR version. 23 | Add any other context about the problem here. 24 | 25 | -------------------------------------------------------------------------------- /examples/api-hooks/pages/api/data.js: -------------------------------------------------------------------------------- 1 | const projects = [ 2 | 'facebook/flipper', 'vuejs/vuepress', 'rust-lang/rust', 'vercel/next.js' 3 | ] 4 | 5 | export default (req, res) => { 6 | if (req.query.id) { 7 | // a slow endpoint for getting repo data 8 | fetch(`https://api.github.com/repos/${req.query.id}`) 9 | .then(resp => resp.json()) 10 | .then(data => { 11 | setTimeout(() => { 12 | res.json(data) 13 | }, 2000) 14 | }) 15 | 16 | return 17 | } 18 | setTimeout(() => { 19 | res.json(projects) 20 | }, 2000) 21 | } 22 | -------------------------------------------------------------------------------- /examples/basic/pages/api/data.js: -------------------------------------------------------------------------------- 1 | const projects = [ 2 | 'facebook/flipper', 'vuejs/vuepress', 'rust-lang/rust', 'vercel/next.js' 3 | ] 4 | 5 | export default function api(req, res) { 6 | if (req.query.id) { 7 | // a slow endpoint for getting repo data 8 | fetch(`https://api.github.com/repos/${req.query.id}`) 9 | .then(resp => resp.json()) 10 | .then(data => { 11 | setTimeout(() => { 12 | res.json(data) 13 | }, 2000) 14 | }) 15 | 16 | return 17 | } 18 | setTimeout(() => { 19 | res.json(projects) 20 | }, 2000) 21 | } 22 | -------------------------------------------------------------------------------- /examples/basic/pages/index.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import fetch from '../libs/fetch' 3 | 4 | import useSWR from 'swr' 5 | 6 | export default function Index() { 7 | const { data } = useSWR('/api/data', fetch) 8 | 9 | return ( 10 |
11 |

Trending Projects

12 |
13 | { 14 | data ? data.map(project => 15 |

{project}

16 | ) : 'loading...' 17 | } 18 |
19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /examples/suspense/app/rsc/repos.jsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import useSWR from 'swr' 3 | import fetcher from '../../libs/fetch' 4 | import Link from 'next/link' 5 | 6 | const Repos = ({ serverData }) => { 7 | const { data } = useSWR('/api/data', fetcher, { 8 | suspense: true, 9 | fallbackData: serverData 10 | }) 11 | return ( 12 | <> 13 | {data.map(project => ( 14 |

15 | 16 | {project} 17 | 18 |

19 | ))} 20 | 21 | ) 22 | } 23 | 24 | export default Repos -------------------------------------------------------------------------------- /examples/global-fetcher/pages/api/data.js: -------------------------------------------------------------------------------- 1 | const projects = [ 2 | 'facebook/flipper', 'vuejs/vuepress', 'rust-lang/rust', 'vercel/next.js' 3 | ] 4 | 5 | export default function api(req, res) { 6 | if (req.query.id) { 7 | // a slow endpoint for getting repo data 8 | fetch(`https://api.github.com/repos/${req.query.id}`) 9 | .then(resp => resp.json()) 10 | .then(data => { 11 | setTimeout(() => { 12 | res.json(data) 13 | }, 2000) 14 | }) 15 | 16 | return 17 | } 18 | setTimeout(() => { 19 | res.json(projects) 20 | }, 2000) 21 | } 22 | -------------------------------------------------------------------------------- /examples/prefetch-preload/pages/api/data.js: -------------------------------------------------------------------------------- 1 | const projects = [ 2 | 'facebook/flipper', 'vuejs/vuepress', 'rust-lang/rust', 'vercel/next.js' 3 | ] 4 | 5 | export default function api(req, res) { 6 | if (req.query.id) { 7 | // a slow endpoint for getting repo data 8 | fetch(`https://api.github.com/repos/${req.query.id}`) 9 | .then(resp => resp.json()) 10 | .then(data => { 11 | setTimeout(() => { 12 | res.json(data) 13 | }, 2000) 14 | }) 15 | 16 | return 17 | } 18 | setTimeout(() => { 19 | res.json(projects) 20 | }, 2000) 21 | } 22 | -------------------------------------------------------------------------------- /e2e/site/app/mutate-server-action/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import useSWRMutation from 'swr/mutation' 3 | import { action } from './action' 4 | 5 | const useServerActionMutation = () => 6 | useSWRMutation('/api/mutate-server-action', () => action()) 7 | 8 | const Page = () => { 9 | const { trigger, data, isMutating } = useServerActionMutation() 10 | return ( 11 |
12 | 13 |
isMutating: {isMutating.toString()}
14 |
data: {data?.result}
15 |
16 | ) 17 | } 18 | 19 | export default Page 20 | -------------------------------------------------------------------------------- /examples/axios/pages/api/data.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const projects = [ 4 | 'facebook/flipper', 'vuejs/vuepress', 'rust-lang/rust', 'vercel/next.js' 5 | ] 6 | 7 | export default function api(req, res) { 8 | if (req.query.id) { 9 | // a slow endpoint for getting repo data 10 | axios(`https://api.github.com/repos/${req.query.id}`) 11 | .then(resp => resp.data) 12 | .then(data => { 13 | setTimeout(() => { 14 | res.json(data) 15 | }, 2000) 16 | }) 17 | return 18 | } 19 | setTimeout(() => { 20 | res.json(projects) 21 | }, 2000) 22 | } 23 | -------------------------------------------------------------------------------- /examples/suspense/app/rsc/[user]/[repo]/repo.jsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import fetcher from '../../../../libs/fetch' 3 | import useSWR from 'swr' 4 | 5 | const Repo = ({ id, serverData }) => { 6 | const { data } = useSWR('/api/data?id=' + id, fetcher, { suspense: true, fallbackData: serverData }) 7 | return ( 8 | <> 9 | {data ? ( 10 |
11 |

forks: {data.forks_count}

12 |

stars: {data.stargazers_count}

13 |

watchers: {data.watchers}

14 |
15 | ) : null} 16 | 17 | 18 | ) 19 | } 20 | 21 | export default Repo -------------------------------------------------------------------------------- /examples/infinite-scroll/hooks/useOnScreen.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | export default function useOnScreen(ref) { 4 | const [isIntersecting, setIntersecting] = useState(false) 5 | 6 | useEffect(() => { 7 | if (!ref.current) return 8 | 9 | const observer = new IntersectionObserver(([entry]) => 10 | setIntersecting(entry.isIntersecting) 11 | ) 12 | 13 | observer.observe(ref.current) 14 | // Remove the observer as soon as the component is unmounted 15 | return () => { 16 | observer.disconnect() 17 | } 18 | }, []) 19 | 20 | return isIntersecting 21 | } 22 | -------------------------------------------------------------------------------- /test/type/suspense/helper-types.tsx: -------------------------------------------------------------------------------- 1 | import type { BlockingData } from 'swr/_internal' 2 | import { expectType } from '../utils' 3 | 4 | declare module 'swr' { 5 | interface SWRGlobalConfig { 6 | suspense: true 7 | } 8 | } 9 | 10 | export function testDataCached() { 11 | expectType>(true) 12 | expectType>(true) 13 | expectType< 14 | BlockingData 15 | >(true) 16 | expectType>( 17 | true 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/test-legacy-react.yml: -------------------------------------------------------------------------------- 1 | name: Test React 17 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - v* 9 | pull_request: 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Install 19 | uses: ./.github/workflows/install 20 | 21 | - name: Test 22 | env: 23 | TEST_REACT_LEGACY: 1 24 | run: | 25 | pnpm clean 26 | pnpm build 27 | pnpm test 28 | pnpm test:build 29 | pnpm test-typing 30 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "strict": false, 6 | "jsx": "react-jsx", 7 | "baseUrl": "..", 8 | "paths": { 9 | "swr": ["./core/src/index.ts"], 10 | "swr/infinite": ["./infinite/src/index.ts"], 11 | "swr/immutable": ["./immutable/src/index.ts"], 12 | "swr/mutation": ["./mutation/src/index.ts"], 13 | "swr/_internal": ["./_internal/src/index.ts"], 14 | "swr/subscription": ["subscription/src/index.ts"] 15 | } 16 | }, 17 | "include": [".", "./jest-setup.ts"], 18 | "exclude": ["./type"] 19 | } 20 | -------------------------------------------------------------------------------- /test/unit/serialize.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment @edge-runtime/jest-environment 3 | */ 4 | import { unstable_serialize } from 'swr' 5 | import { stableHash } from 'swr/_internal' 6 | 7 | describe('SWR - unstable_serialize', () => { 8 | it('should serialize arguments correctly', async () => { 9 | expect(unstable_serialize([])).toBe('') 10 | expect(unstable_serialize(null)).toBe('') 11 | expect(unstable_serialize('key')).toBe('key') 12 | expect(unstable_serialize([1, { foo: 2, bar: 1 }, ['a', 'b', 'c']])).toBe( 13 | stableHash([1, { foo: 2, bar: 1 }, ['a', 'b', 'c']]) 14 | ) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/_internal/utils/normalize-args.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from './shared' 2 | 3 | import type { Key, Fetcher, SWRConfiguration } from '../types' 4 | 5 | export const normalize = ( 6 | args: 7 | | [KeyType] 8 | | [KeyType, Fetcher | null] 9 | | [KeyType, SWRConfiguration | undefined] 10 | | [KeyType, Fetcher | null, SWRConfiguration | undefined] 11 | ): [KeyType, Fetcher | null, Partial>] => { 12 | return isFunction(args[1]) 13 | ? [args[0], args[1], args[2] || {}] 14 | : [args[0], null, (args[1] === null ? args[2] : args[1]) || {}] 15 | } 16 | -------------------------------------------------------------------------------- /e2e/site/app/partially-hydrate/layout.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import type { PropsWithChildren } from 'react' 3 | import { useDebugHistory } from '~/lib/use-debug-history' 4 | import useData from './use-data' 5 | 6 | export default function Layout({ children }: PropsWithChildren) { 7 | const { data } = useData() 8 | const debugRef = useDebugHistory(data, 'first history:') 9 | return ( 10 | 11 | 12 | 13 |
14 |
15 | <>first data:{data || 'undefined'} 16 |
17 | {children} 18 | 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /examples/global-fetcher/pages/index.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | import useSWR from 'swr' 4 | 5 | export default function Index() { 6 | const { data } = useSWR('/api/data') 7 | 8 | return ( 9 |
10 |

Trending Projects

11 |
12 | {data 13 | ? data.map(project => ( 14 |

15 | 16 | {project} 17 | 18 |

19 | )) 20 | : 'loading...'} 21 |
22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /examples/storage-tab-sync/pages/index.js: -------------------------------------------------------------------------------- 1 | import storage from '../libs/storage' 2 | 3 | import useSWR, { mutate } from 'swr' 4 | 5 | export default function Index() { 6 | const { data = { name: "" } } = useSWR('user-name', storage) 7 | 8 | function handleChange(event) { 9 | localStorage.setItem( 10 | 'user-name', 11 | JSON.stringify({ name: event.target.value }) 12 | ) 13 | mutate('user-name', { name: event.target.value }) 14 | } 15 | 16 | return
17 | 18 | 19 |
20 | } 21 | -------------------------------------------------------------------------------- /examples/axios/pages/index.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | import useRequest from '../libs/useRequest' 4 | 5 | export default function Index() { 6 | const { data } = useRequest({ 7 | url: '/api/data' 8 | }) 9 | 10 | return ( 11 |
12 |

Trending Projects

13 |
14 | {data 15 | ? data.map(project => ( 16 |

17 | 18 | {project} 19 | 20 |

21 | )) 22 | : 'loading...'} 23 |
24 |
25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/test-canary.yml: -------------------------------------------------------------------------------- 1 | name: Test React Canary 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 0 * * *' 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | 15 | - name: Install 16 | uses: ./.github/workflows/install 17 | 18 | - name: Install Canary 19 | run: corepack pnpm upgrade react@canary react-dom@canary use-sync-external-store@canary 20 | 21 | - name: Lint and test 22 | run: | 23 | pnpm clean 24 | pnpm build 25 | pnpm test 26 | pnpm test:build 27 | pnpm test-typing 28 | -------------------------------------------------------------------------------- /examples/axios-typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve" 20 | }, 21 | "exclude": [ 22 | "node_modules" 23 | ], 24 | "include": [ 25 | "next-env.d.ts", 26 | "**/*.ts", 27 | "**/*.tsx" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /examples/basic-typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve" 20 | }, 21 | "exclude": [ 22 | "node_modules" 23 | ], 24 | "include": [ 25 | "next-env.d.ts", 26 | "**/*.ts", 27 | "**/*.tsx" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/install/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Install' 2 | description: 'Set up and install dependencies' 3 | runs: 4 | using: composite 5 | steps: 6 | - name: Setup pnpm 7 | uses: pnpm/action-setup@v4 8 | 9 | - name: Lock Corepack version 10 | shell: bash 11 | run: pnpm i -g corepack@0.31.0 12 | 13 | - name: Use Node.js lts 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: 22 17 | cache: pnpm 18 | registry-url: 'https://registry.npmjs.org' 19 | 20 | - name: Install Dependencies 21 | shell: bash 22 | run: | 23 | corepack enable 24 | node -v 25 | pnpm -v 26 | pnpm install 27 | -------------------------------------------------------------------------------- /examples/axios/README.md: -------------------------------------------------------------------------------- 1 | # Axios 2 | 3 | ## One-Click Deploy 4 | 5 | Deploy your own SWR project with Vercel. 6 | 7 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/axios) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/axios 15 | cd axios 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Show a basic usage of SWR fetching using axios and a request object. 31 | -------------------------------------------------------------------------------- /examples/suspense/README.md: -------------------------------------------------------------------------------- 1 | # Basic 2 | 3 | ## One-Click Deploy 4 | 5 | Deploy your own SWR project with Vercel. 6 | 7 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/suspense) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/suspense 15 | cd suspense 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Show how to use the SWR suspense option with React suspense. 31 | -------------------------------------------------------------------------------- /examples/suspense/app/rsc/[user]/[repo]/page.jsx: -------------------------------------------------------------------------------- 1 | import Repo from './repo' 2 | import fetcher from '../../../../libs/fetch' 3 | import Link from 'next/link' 4 | import { Suspense } from 'react' 5 | const Page = ({ params }) => { 6 | const { user, repo } = params 7 | const id = `${user}/${repo}` 8 | const serverData = fetcher('http://localhost:3000/api/data?id=' + id) 9 | return ( 10 |
11 |
Repo: {id}
12 | Loading stats
}> 13 | 14 | 15 |
16 |
17 | Back 18 | 19 | ) 20 | } 21 | 22 | 23 | export default Page -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 | # Basic 2 | 3 | ## One-Click Deploy 4 | 5 | Deploy your own SWR project with Vercel. 6 | 7 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/basic) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/basic 15 | cd basic 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Show a basic usage of SWR fetching data from an API in two different pages. 31 | -------------------------------------------------------------------------------- /examples/infinite/README.md: -------------------------------------------------------------------------------- 1 | # useSWRInfinite 2 | 3 | ## One-Click Deploy 4 | 5 | Deploy your own SWR project with Vercel. 6 | 7 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/infinite) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/infinite 15 | cd basic 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Show usage of useSWRInfinite with load more data button 31 | -------------------------------------------------------------------------------- /examples/axios-typescript/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | import useRequest from '../libs/useRequest' 4 | 5 | export default function Index() { 6 | const { data } = useRequest({ 7 | url: '/api/data' 8 | }) 9 | 10 | return ( 11 |
12 |

Trending Projects

13 |
14 | {data 15 | ? data.map(project => ( 16 |

17 | 18 | {project} 19 | 20 |

21 | )) 22 | : 'loading...'} 23 |
24 |
25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /examples/basic-typescript/pages/api/data.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next' 2 | 3 | const projects = [ 4 | 'facebook/flipper', 5 | 'vuejs/vuepress', 6 | 'rust-lang/rust', 7 | 'vercel/next.js' 8 | ] 9 | 10 | export default function api(req: NextApiRequest, res: NextApiResponse) { 11 | if (req.query.id) { 12 | // a slow endpoint for getting repo data 13 | fetch(`https://api.github.com/repos/${req.query.id}`) 14 | .then(resp => resp.json()) 15 | .then(data => { 16 | setTimeout(() => { 17 | res.json(data) 18 | }, 2000) 19 | }) 20 | 21 | return 22 | } 23 | setTimeout(() => { 24 | res.json(projects) 25 | }, 2000) 26 | } 27 | -------------------------------------------------------------------------------- /examples/suspense-global/README.md: -------------------------------------------------------------------------------- 1 | # Basic 2 | 3 | ## One-Click Deploy 4 | 5 | Deploy your own SWR project with Vercel. 6 | 7 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/suspense) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/suspense 15 | cd suspense 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Show how to use the SWR suspense option with React suspense. 31 | -------------------------------------------------------------------------------- /examples/global-fetcher/pages/[user]/[repo].js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | import useSWR from 'swr' 4 | 5 | export default function Repo() { 6 | const id = typeof window !== 'undefined' ? window.location.pathname.slice(1) : '' 7 | const { data } = useSWR('/api/data?id=' + id) 8 | 9 | return ( 10 |
11 |

{id}

12 | { 13 | data ?
14 |

forks: {data.forks_count}

15 |

stars: {data.stargazers_count}

16 |

watchers: {data.watchers}

17 |
: 'loading...' 18 | } 19 |
20 |
21 | Back 22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /e2e/site/app/partially-hydrate/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useDebugHistory } from '~/lib/use-debug-history' 3 | import useData from './use-data' 4 | 5 | let resolved = false 6 | const susp = new Promise(res => { 7 | setTimeout(() => { 8 | resolved = true 9 | res(true) 10 | }, 2000) 11 | }) 12 | 13 | export default function Page() { 14 | // We trigger the suspense boundary here! 15 | if (!resolved) { 16 | throw susp 17 | } 18 | 19 | const { data } = useData() 20 | const debugRef = useDebugHistory(data, 'second history:') 21 | return ( 22 |
23 |
24 | <>second data (delayed hydration):{data || 'undefined'} 25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /examples/api-hooks/pages/[user]/[repo].js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import useRepository from '../../hooks/use-repository' 3 | 4 | export default function Repo() { 5 | const id = typeof window !== 'undefined' ? window.location.pathname.slice(1) : '' 6 | const { data } = useRepository(id) 7 | 8 | return ( 9 |
10 |

{id}

11 | { 12 | data ?
13 |

forks: {data.forks_count}

14 |

stars: {data.stargazers_count}

15 |

watchers: {data.watchers}

16 |
: 'loading...' 17 | } 18 |
19 |
20 | Back 21 |
22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /examples/subscription/README.md: -------------------------------------------------------------------------------- 1 | # Subscription 2 | 3 | ## One-Click Deploy 4 | 5 | Deploy your own SWR project with Vercel. 6 | 7 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/subscription) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/subscription 15 | cd subscription 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Show how you could use SWR to subscribe async observable data into your app. 31 | -------------------------------------------------------------------------------- /src/_internal/utils/subscribe-key.ts: -------------------------------------------------------------------------------- 1 | type Callback = (...args: any[]) => any 2 | 3 | // Add a callback function to a list of keyed callback functions and return 4 | // the unsubscribe function. 5 | export const subscribeCallback = ( 6 | key: string, 7 | callbacks: Record, 8 | callback: Callback 9 | ) => { 10 | const keyedRevalidators = callbacks[key] || (callbacks[key] = []) 11 | keyedRevalidators.push(callback) 12 | 13 | return () => { 14 | const index = keyedRevalidators.indexOf(callback) 15 | 16 | if (index >= 0) { 17 | // O(1): faster than splice 18 | keyedRevalidators[index] = keyedRevalidators[keyedRevalidators.length - 1] 19 | keyedRevalidators.pop() 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/axios-typescript/pages/api/data.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next' 2 | import axios from 'axios' 3 | 4 | const projects = [ 5 | 'facebook/flipper', 6 | 'vuejs/vuepress', 7 | 'rust-lang/rust', 8 | 'vercel/next.js' 9 | ] 10 | 11 | export default function api(req: NextApiRequest, res: NextApiResponse) { 12 | if (req.query.id) { 13 | // a slow endpoint for getting repo data 14 | axios(`https://api.github.com/repos/${req.query.id}`) 15 | .then(response => response.data) 16 | .then(data => { 17 | setTimeout(() => { 18 | res.json(data) 19 | }, 2000) 20 | }) 21 | 22 | return 23 | } 24 | setTimeout(() => { 25 | res.json(projects) 26 | }, 2000) 27 | } 28 | -------------------------------------------------------------------------------- /examples/local-state-sharing/README.md: -------------------------------------------------------------------------------- 1 | # Basic 2 | 3 | ## One-Click Deploy 4 | 5 | Deploy your own SWR project with Vercel. 6 | 7 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/local-state-sharing) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/local-state-sharing 15 | cd local-state-sharing 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Show how to share local state between React components using SWR. 31 | -------------------------------------------------------------------------------- /examples/basic/pages/[user]/[repo].js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import fetch from '../../libs/fetch' 3 | 4 | import useSWR from 'swr' 5 | 6 | export default function Repo() { 7 | const id = typeof window !== 'undefined' ? window.location.pathname.slice(1) : '' 8 | const { data } = useSWR('/api/data?id=' + id, fetch) 9 | 10 | return ( 11 |
12 |

{id}

13 | { 14 | data ?
15 |

forks: {data.forks_count}

16 |

stars: {data.stargazers_count}

17 |

watchers: {data.watchers}

18 |
: 'loading...' 19 | } 20 |
21 |
22 | Back 23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /examples/global-fetcher/README.md: -------------------------------------------------------------------------------- 1 | # Global Fetcher 2 | 3 | ## One-Click Deploy 4 | 5 | Deploy your own SWR project with Vercel. 6 | 7 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/global-fetcher) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/global-fetcher 15 | cd global-fetcher 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Use the `SWRConfig` provider to set up the fetcher globally instead of a per-hook call. 31 | -------------------------------------------------------------------------------- /examples/storage-tab-sync/README.md: -------------------------------------------------------------------------------- 1 | # Storage Tab Sync 2 | 3 | ## One-Click Deploy 4 | 5 | Deploy your own SWR project with Vercel. 6 | 7 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/storage-tab-sync) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/storage-tab-sync 15 | cd storage-tab-sync 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Show how you could use SWR to synchronize localStorage values between tabs. 31 | -------------------------------------------------------------------------------- /src/_internal/utils/merge-config.ts: -------------------------------------------------------------------------------- 1 | import { mergeObjects } from './shared' 2 | import type { FullConfiguration } from '../types' 3 | 4 | export const mergeConfigs = ( 5 | a: Partial, 6 | b?: Partial 7 | ) => { 8 | // Need to create a new object to avoid mutating the original here. 9 | const v: Partial = mergeObjects(a, b) 10 | 11 | // If two configs are provided, merge their `use` and `fallback` options. 12 | if (b) { 13 | const { use: u1, fallback: f1 } = a 14 | const { use: u2, fallback: f2 } = b 15 | if (u1 && u2) { 16 | v.use = u1.concat(u2) 17 | } 18 | if (f1 && f2) { 19 | v.fallback = mergeObjects(f1, f2) 20 | } 21 | } 22 | 23 | return v 24 | } 25 | -------------------------------------------------------------------------------- /examples/basic-typescript/README.md: -------------------------------------------------------------------------------- 1 | # Basic TypeScript 2 | 3 | ## One-Click Deploy 4 | 5 | Deploy your own SWR project with Vercel. 6 | 7 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/basic-typescript) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/basic-typescript 15 | cd basic-typescript 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Show how to use the basic example along with TypeScript to type the data received from SWR. 31 | -------------------------------------------------------------------------------- /examples/api-hooks/README.md: -------------------------------------------------------------------------------- 1 | # API Hooks 2 | 3 | ## One-Click Deploy 4 | 5 | Deploy your own SWR project with Vercel. 6 | 7 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/api-hooks) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/api-hooks 15 | cd api-hooks 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Show how you could create custom hooks, using SWR internally, for your different data requirements and use them in your application. 31 | -------------------------------------------------------------------------------- /examples/basic-typescript/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import fetch from '../libs/fetch' 3 | 4 | import useSWR from 'swr' 5 | 6 | export default function HomePage() { 7 | const { data } = useSWR('/api/data', fetch) 8 | const { data: data2 } = useSWR(null, fetch) 9 | 10 | return ( 11 |
12 |

Trending Projects

13 | {data2} 14 |
15 | {data 16 | ? data.map(project => ( 17 |

18 | 19 | {project} 20 | 21 |

22 | )) 23 | : 'loading...'} 24 |
25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /examples/autocomplete-suggestions/README.md: -------------------------------------------------------------------------------- 1 | # Autocomplete Suggestions 2 | 3 | ## One-Click Deploy 4 | 5 | Deploy your own SWR project with Vercel. 6 | 7 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/autocomplete-suggestions) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/autocomplete-suggestions 15 | cd autocomplete-suggestions 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Show how to use SWR to fetch the suggestion for an autocomplete. 31 | -------------------------------------------------------------------------------- /examples/refetch-interval/README.md: -------------------------------------------------------------------------------- 1 | # Refetch Interval 2 | 3 | ## One-Click Deploy 4 | 5 | Deploy your own SWR project with Vercel Now. 6 | 7 | [![Deploy with Vercel Now](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/refetch-interval) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/refetch-interval 15 | cd refetch-interval 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Show how to make SWR fetch the API again in an interval automatically to ensure the data is up-to-date. 31 | -------------------------------------------------------------------------------- /examples/axios-typescript/README.md: -------------------------------------------------------------------------------- 1 | # Axios TypeScript 2 | 3 | ## One-Click Deploy 4 | 5 | Deploy your own SWR project with Vercel. 6 | 7 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/axios-typescript) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/axios-typescript 15 | cd axios-typescript 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Show how to use the basic axios along with TypeScript to type both the request object and the data received from SWR. 31 | -------------------------------------------------------------------------------- /examples/infinite-scroll/README.md: -------------------------------------------------------------------------------- 1 | # useSWRInfinite with scroll based on IntersectionObserver 2 | 3 | ## One-Click Deploy 4 | 5 | Deploy your own SWR project with Vercel. 6 | 7 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/infinite-scroll) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/infinite-scroll 15 | cd infinite-scroll 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Show usage of useSWRInfinite with infinite scroll based on IntersectionObserver 31 | -------------------------------------------------------------------------------- /examples/suspense/pages/api/data.js: -------------------------------------------------------------------------------- 1 | const projects = [ 2 | 'facebook/flipper', 3 | 'vuejs/vuepress', 4 | 'rust-lang/rust', 5 | 'vercel/next.js', 6 | 'emperor/clothes' 7 | ] 8 | 9 | export default function api(req, res) { 10 | if (req.query.id) { 11 | if (req.query.id === projects[4]) { 12 | setTimeout(() => { 13 | res.json({ msg: 'not found' }) 14 | }) 15 | } else { 16 | // a slow endpoint for getting repo data 17 | fetch(`https://api.github.com/repos/${req.query.id}`) 18 | .then(res => res.json()) 19 | .then(data => { 20 | setTimeout(() => { 21 | res.json(data) 22 | }, 2000) 23 | }) 24 | } 25 | } else { 26 | setTimeout(() => { 27 | res.json(projects) 28 | }, 2000) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/_internal/utils/serialize.ts: -------------------------------------------------------------------------------- 1 | import { stableHash } from './hash' 2 | import { isFunction } from './shared' 3 | 4 | import type { Key, Arguments } from '../types' 5 | 6 | export const serialize = (key: Key): [string, Arguments] => { 7 | if (isFunction(key)) { 8 | try { 9 | key = key() 10 | } catch (err) { 11 | // dependencies not ready 12 | key = '' 13 | } 14 | } 15 | 16 | // Use the original key as the argument of fetcher. This can be a string or an 17 | // array of values. 18 | const args = key 19 | 20 | // If key is not falsy, or not an empty array, hash it. 21 | key = 22 | typeof key == 'string' 23 | ? key 24 | : (Array.isArray(key) ? key.length : key) 25 | ? stableHash(key) 26 | : '' 27 | 28 | return [key, args] 29 | } 30 | -------------------------------------------------------------------------------- /e2e/test/suspense-undefined-key.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test' 2 | 3 | test.describe('suspense with undefined key', () => { 4 | test('should render correctly when key is undefined', async ({ page }) => { 5 | await page.goto('./suspense-undefined-key', { waitUntil: 'commit' }) 6 | 7 | // Should show content for undefined key (not suspense) 8 | await expect(page.getByText('empty')).toBeVisible() 9 | 10 | // Click toggle to enable key 11 | await page.getByRole('button', { name: 'toggle' }).click() 12 | 13 | // Should show loading fallback when key becomes defined 14 | await expect(page.getByText('fallback')).toBeVisible() 15 | 16 | // Should eventually show the fetched data 17 | await expect(page.getByText('SWR')).toBeVisible() 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /examples/suspense-retry/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "baseUrl": ".", 23 | "paths": { 24 | "~/*": ["./*"] 25 | } 26 | }, 27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 28 | "exclude": ["node_modules"] 29 | } 30 | -------------------------------------------------------------------------------- /src/_internal/utils/with-middleware.ts: -------------------------------------------------------------------------------- 1 | import { normalize } from './normalize-args' 2 | 3 | import type { 4 | Key, 5 | Fetcher, 6 | Middleware, 7 | SWRConfiguration, 8 | SWRHook 9 | } from '../types' 10 | 11 | // Create a custom hook with a middleware 12 | export const withMiddleware = ( 13 | useSWR: SWRHook, 14 | middleware: Middleware 15 | ): SWRHook => { 16 | return ( 17 | ...args: 18 | | [Key] 19 | | [Key, Fetcher | null] 20 | | [Key, SWRConfiguration | undefined] 21 | | [Key, Fetcher | null, SWRConfiguration | undefined] 22 | ) => { 23 | const [key, fn, config] = normalize(args) 24 | const uses = (config.use || []).concat(middleware) 25 | return useSWR(key, fn, { ...config, use: uses }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/focus-revalidate/README.md: -------------------------------------------------------------------------------- 1 | # Focus Revalidate 2 | 3 | ## One-Click Deploy 4 | 5 | Deploy your own SWR project with Vercel. 6 | 7 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/focus-revalidate) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/focus-revalidate 15 | cd focus-revalidate 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Basic authentication example showing how the revalidate on focus feature works and to trigger a revalidation on a per-hook call basis. 31 | -------------------------------------------------------------------------------- /e2e/site/app/concurrent-transition/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Suspense } from 'react' 4 | import dynamic from 'next/dynamic' 5 | 6 | const TransitionDemo = dynamic(() => import('./transition-demo'), { 7 | ssr: false 8 | }) 9 | 10 | export default function ConcurrentTransitionPage() { 11 | return ( 12 |
13 |

React 19 Concurrent Transition Test

14 |

15 | This page tests SWR's behavior with React 19 concurrent 16 | transitions. When using useTransition, SWR should "pause" 17 | loading states to provide smooth UX. 18 |

19 | Loading page...
}> 20 | 21 | 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /examples/optimistic-ui/README.md: -------------------------------------------------------------------------------- 1 | # Optimistic UI 2 | 3 | ## One-Click Deploy 4 | 5 | Deploy your own SWR project with Vercel. 6 | 7 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/optimistic-ui) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/optimistic-ui 15 | cd optimistic-ui 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Example of how to use SWR to implement an Optimistic UI pattern where we mutate the cached data immediately and then trigger a revalidation with the API. 31 | -------------------------------------------------------------------------------- /examples/suspense-global/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "noImplicitAny": true, 18 | "plugins": [ 19 | { 20 | "name": "next" 21 | } 22 | ], 23 | "baseUrl": ".", 24 | "paths": { 25 | "~/*": ["./*"] 26 | } 27 | }, 28 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 29 | "exclude": ["node_modules"] 30 | } 31 | -------------------------------------------------------------------------------- /examples/focus-revalidate/pages/index.js: -------------------------------------------------------------------------------- 1 | import Button from '../components/button' 2 | import fetch from '../libs/fetch' 3 | import { login, logout } from '../libs/auth' 4 | 5 | import useSWR from 'swr' 6 | 7 | export default function Index() { 8 | const { data, mutate } = useSWR('/api/user', fetch) 9 | 10 | if (!data) return

loading...

11 | if (data.loggedIn) { 12 | return
13 |

Welcome, {data.name}

14 | 15 | 19 |
20 | } else { 21 | return
22 |

Please login

23 | 27 |
28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/optimistic-ui-immer/README.md: -------------------------------------------------------------------------------- 1 | # Optimistic UI with Immer 2 | 3 | ## One-Click Deploy 4 | 5 | Deploy your own SWR project with Vercel. 6 | 7 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/optimistic-ui-immer) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/optimistic-ui-immer 15 | cd optimistic-ui-immer 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Example of how to use SWR and Immer to implement an Optimistic UI pattern where we mutate the cached data immediately and then trigger a revalidation with the API. 31 | -------------------------------------------------------------------------------- /e2e/site/app/suspense-undefined-key/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Suspense, useReducer } from 'react' 4 | import useSWR from 'swr' 5 | 6 | const fetcher = async (key: string) => { 7 | // Add a small delay to simulate network request 8 | await new Promise(resolve => setTimeout(resolve, 100)) 9 | return 'SWR' 10 | } 11 | 12 | const Section = ({ trigger }: { trigger: boolean }) => { 13 | const { data } = useSWR(trigger ? 'test-key' : undefined, fetcher, { 14 | suspense: true 15 | }) 16 | return
{data || 'empty'}
17 | } 18 | 19 | export default function Page() { 20 | const [trigger, toggle] = useReducer(x => !x, false) 21 | 22 | return ( 23 |
24 | 25 | fallback
}> 26 |
27 | 28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/index/index.ts: -------------------------------------------------------------------------------- 1 | // useSWR 2 | import useSWR from './use-swr' 3 | export default useSWR 4 | // Core APIs 5 | export { SWRConfig } from './use-swr' 6 | export { unstable_serialize } from './serialize' 7 | export { useSWRConfig } from '../_internal' 8 | export { mutate } from '../_internal' 9 | export { preload } from '../_internal' 10 | 11 | // Config 12 | 13 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 14 | export interface SWRGlobalConfig { 15 | // suspense: true 16 | } 17 | 18 | // Types 19 | export type { 20 | SWRConfiguration, 21 | Revalidator, 22 | RevalidatorOptions, 23 | Key, 24 | KeyLoader, 25 | KeyedMutator, 26 | SWRHook, 27 | SWRResponse, 28 | Cache, 29 | BareFetcher, 30 | Fetcher, 31 | MutatorCallback, 32 | MutatorOptions, 33 | Middleware, 34 | Arguments, 35 | State, 36 | ScopedMutator 37 | } from '../_internal' 38 | -------------------------------------------------------------------------------- /examples/basic-typescript/pages/[user]/[repo].tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import fetch from '../../libs/fetch' 3 | 4 | import useSWR from 'swr' 5 | 6 | export default function Repo() { 7 | const id = 8 | typeof window !== 'undefined' ? window.location.pathname.slice(1) : '' 9 | const { data } = useSWR<{ 10 | forks_count: number 11 | stargazers_count: number 12 | watchers: number 13 | }>('/api/data?id=' + id, fetch) 14 | 15 | return ( 16 |
17 |

{id}

18 | {data ? ( 19 |
20 |

forks: {data.forks_count}

21 |

stars: {data.stargazers_count}

22 |

watchers: {data.watchers}

23 |
24 | ) : ( 25 | 'loading...' 26 | )} 27 |
28 |
29 | Back 30 |
31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "esModuleInterop": true, 5 | "jsx": "react", 6 | "lib": ["esnext", "dom"], 7 | "module": "NodeNext", 8 | "moduleResolution": "nodenext", 9 | "noFallthroughCasesInSwitch": true, 10 | "noImplicitReturns": false, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "strictBindCallApply": true, 14 | "outDir": "./dist", 15 | "rootDir": "./", 16 | "strict": true, 17 | "target": "ES2018", 18 | "noEmitOnError": true, 19 | "downlevelIteration": true, 20 | "incremental": true 21 | }, 22 | "include": ["./src/**/*", "env.d.ts", "eslint.config.mjs"], 23 | "exclude": ["./**/dist", "examples"], 24 | "watchOptions": { 25 | "watchFile": "useFsEvents", 26 | "watchDirectory": "useFsEvents", 27 | "fallbackPolling": "dynamicPriority" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/_internal/utils/shared.ts: -------------------------------------------------------------------------------- 1 | // Shared state between server components and client components 2 | 3 | export const noop = () => {} 4 | 5 | // Using noop() as the undefined value as undefined can be replaced 6 | // by something else. Prettier ignore and extra parentheses are necessary here 7 | // to ensure that tsc doesn't remove the __NOINLINE__ comment. 8 | // prettier-ignore 9 | export const UNDEFINED = (/*#__NOINLINE__*/ noop()) as undefined 10 | 11 | export const OBJECT = Object 12 | 13 | export const isUndefined = (v: any): v is undefined => v === UNDEFINED 14 | export const isFunction = < 15 | T extends (...args: any[]) => any = (...args: any[]) => any 16 | >( 17 | v: unknown 18 | ): v is T => typeof v == 'function' 19 | export const mergeObjects = (a: any, b?: any) => ({ ...a, ...b }) 20 | export const isPromiseLike = (x: unknown): x is PromiseLike => 21 | isFunction((x as any).then) 22 | -------------------------------------------------------------------------------- /examples/axios-typescript/pages/[user]/[repo].tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | import useRequest from '../../libs/useRequest' 4 | 5 | export default function Repo() { 6 | const id = 7 | typeof window !== 'undefined' ? window.location.pathname.slice(1) : '' 8 | const { data } = useRequest<{ 9 | forks_count: number 10 | stargazers_count: number 11 | watchers: number 12 | }>({ 13 | url: '/api/data', 14 | params: { id } 15 | }) 16 | 17 | return ( 18 |
19 |

{id}

20 | {data ? ( 21 |
22 |

forks: {data.forks_count}

23 |

stars: {data.stargazers_count}

24 |

watchers: {data.watchers}

25 |
26 | ) : ( 27 | 'loading...' 28 | )} 29 |
30 |
31 | Back 32 |
33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /examples/axios/pages/[user]/[repo].js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | import useRequest from '../../libs/useRequest' 4 | 5 | export default function Repo() { 6 | const id = 7 | typeof window !== 'undefined' ? window.location.pathname.slice(1) : '' 8 | const { data } = useRequest( 9 | id 10 | ? { 11 | url: '/api/data', 12 | params: { 13 | id 14 | } 15 | } 16 | : null 17 | ) 18 | 19 | return ( 20 |
21 |

{id}

22 | {data ? ( 23 |
24 |

forks: {data.forks_count}

25 |

stars: {data.stargazers_count}

26 |

watchers: {data.watchers}

27 |
28 | ) : ( 29 | 'loading...' 30 | )} 31 |
32 |
33 | 34 | Back 35 | 36 |
37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /examples/suspense-global/pages/[user]/[repo].tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | import Link from 'next/link' 3 | import { Suspense } from 'react' 4 | import ErrorHandling from '../../components/error-handling' 5 | import { useRouter } from 'next/router' 6 | 7 | const Detail = dynamic(() => import('./detail'), { 8 | ssr: false 9 | }) 10 | 11 | export default function Repo() { 12 | const router = useRouter() 13 | if (!router.isReady) return null 14 | const { user, repo } = router.query 15 | const id = `${user}/${repo}` 16 | 17 | return ( 18 |
19 |

{id}

20 | loading...
}> 21 | oooops!}> 22 | 23 | 24 | 25 |
26 |
27 | Back 28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /examples/server-render/pages/index.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import fetcher from '../libs/fetcher' 3 | 4 | import useSWR from 'swr' 5 | 6 | const URL = 'https://pokeapi.co/api/v2/pokemon/' 7 | 8 | export default function Home({ fallbackData }) { 9 | const { data } = useSWR(URL, fetcher, { fallbackData }) 10 | 11 | return ( 12 |
13 |

Trending Projects

14 |
15 | {data && data.results 16 | ? data.results.map(pokemon => ( 17 |

18 | 19 | {pokemon.name} 20 | 21 |

22 | )) 23 | : 'loading...'} 24 |
25 |
26 | ) 27 | } 28 | 29 | export async function getServerSideProps() { 30 | const data = await fetcher(URL) 31 | return { props: { fallbackData: data } } 32 | } -------------------------------------------------------------------------------- /e2e/site/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "react-jsx", 20 | "incremental": true, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ], 26 | "baseUrl": ".", 27 | "paths": { 28 | "~/*": [ 29 | "./*" 30 | ] 31 | } 32 | }, 33 | "include": [ 34 | "next-env.d.ts", 35 | "**/*.ts", 36 | "**/*.tsx", 37 | ".next/types/**/*.ts", 38 | ".next/dev/types/**/*.ts" 39 | ], 40 | "exclude": [ 41 | "node_modules" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /examples/optimistic-ui/pages/api/todos.js: -------------------------------------------------------------------------------- 1 | let todos = [] 2 | const delay = () => new Promise(res => setTimeout(() => res(), 1000)) 3 | 4 | async function getTodos() { 5 | await delay() 6 | return todos.sort((a, b) => (a.text < b.text ? -1 : 1)) 7 | } 8 | 9 | async function addTodo(todo) { 10 | await delay() 11 | // Sometimes it will fail, this will cause a regression on the UI 12 | if (Math.random() < 0.2 || !todo.text) 13 | throw new Error('Failed to add new item!') 14 | todo.text = todo.text.charAt(0).toUpperCase() + todo.text.slice(1) 15 | todos = [...todos, todo] 16 | return todo 17 | } 18 | 19 | export default async function api(req, res) { 20 | try { 21 | if (req.method === 'POST') { 22 | const body = JSON.parse(req.body) 23 | return res.json(await addTodo(body)) 24 | } 25 | 26 | return res.json(await getTodos()) 27 | } catch (err) { 28 | return res.status(500).json({ error: err.message }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /e2e/site/app/issue-2702/reproduction.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import useSWR, { preload } from 'swr' 3 | import { Suspense, use, useEffect, useState } from 'react' 4 | 5 | const sleep = (time: number, data: string) => 6 | new Promise(resolve => { 7 | setTimeout(() => resolve(data), time) 8 | }) 9 | 10 | const Bug = () => { 11 | const a = use(preload('a', () => sleep(1000, 'a'))) 12 | const { data: b } = useSWR('b', () => sleep(2000, 'b'), { 13 | suspense: true 14 | }) 15 | useState(b) 16 | return ( 17 |
18 | {a},{b} 19 |
20 | ) 21 | } 22 | 23 | const Comp = () => { 24 | const [loading, setLoading] = useState(true) 25 | 26 | // To prevent SSR 27 | useEffect(() => { 28 | setLoading(false) 29 | }, []) 30 | 31 | if (loading) { 32 | return Loading... 33 | } 34 | return ( 35 | fetching}> 36 | 37 | 38 | ) 39 | } 40 | 41 | export default Comp 42 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'jsdom', 3 | testRegex: '/test/.*\\.test\\.tsx?$', 4 | testPathIgnorePatterns: ['/node_modules/', '/e2e/'], 5 | modulePathIgnorePatterns: ['/examples/'], 6 | setupFilesAfterEnv: ['/test/jest-setup.ts'], 7 | moduleNameMapper: { 8 | '^swr$': '/src/index/index.ts', 9 | '^swr/infinite$': '/src/infinite/index.ts', 10 | '^swr/immutable$': '/src/immutable/index.ts', 11 | '^swr/subscription$': '/src/subscription/index.ts', 12 | '^swr/mutation$': '/src/mutation/index.ts', 13 | '^swr/_internal$': '/src/_internal/index.ts', 14 | }, 15 | transform: { 16 | '^.+\\.(t|j)sx?$': ['@swc/jest'] 17 | }, 18 | coveragePathIgnorePatterns: [ 19 | '/node_modules/', 20 | '/dist/', 21 | '/test/', 22 | '/src/_internal/utils/env.ts', 23 | ], 24 | coverageReporters: ['text', 'html'], 25 | reporters: [['github-actions', { silent: false }], 'summary'] 26 | } 27 | -------------------------------------------------------------------------------- /e2e/site/component/manual-retry.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react' 2 | import { ErrorBoundary } from 'react-error-boundary' 3 | import { useRemoteData, preloadRemote } from './use-remote-data' 4 | 5 | const Demo = () => { 6 | const { data } = useRemoteData() 7 | return
data: {data}
8 | } 9 | 10 | function Fallback({ resetErrorBoundary }: any) { 11 | return ( 12 |
13 |

Something went wrong

14 | 21 |
22 | ) 23 | } 24 | 25 | function RemoteData() { 26 | return ( 27 |
28 | { 31 | preloadRemote() 32 | }} 33 | > 34 | loading
}> 35 | 36 | 37 | 38 | 39 | ) 40 | } 41 | 42 | export default RemoteData 43 | -------------------------------------------------------------------------------- /examples/server-render/README.md: -------------------------------------------------------------------------------- 1 | # Server Render 2 | 3 | ## One-Click Deploy 4 | 5 | Deploy your own SWR project with Vercel. 6 | 7 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/server-render) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/server-render 15 | cd server-render 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | This example shows how to combine Next.js getServerSideProps with the SWR `fallbackData` option to support Server-Side Rendering. 31 | 32 | The application will fetch the data server-side and then receive it as props, that data will be passed as `fallbackData` to SWR, once the application starts client-side SWR will revalidate it against the API and update the DOM, if it's required, with the new data. 33 | -------------------------------------------------------------------------------- /e2e/site/app/suspense-retry/manual-retry.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { Suspense } from 'react' 3 | import { ErrorBoundary } from 'react-error-boundary' 4 | import { useRemoteData, preloadRemote } from './use-remote-data' 5 | 6 | const Demo = () => { 7 | const { data } = useRemoteData() 8 | return
data: {data}
9 | } 10 | 11 | function Fallback({ resetErrorBoundary }: any) { 12 | return ( 13 |
14 |

Something went wrong

15 | 22 |
23 | ) 24 | } 25 | 26 | function RemoteData() { 27 | return ( 28 |
29 | { 32 | preloadRemote() 33 | }} 34 | > 35 | loading
}> 36 | 37 | 38 | 39 | 40 | ) 41 | } 42 | 43 | export default RemoteData 44 | -------------------------------------------------------------------------------- /examples/suspense-retry/app/manual-retry.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { Suspense } from 'react' 3 | import { ErrorBoundary } from 'react-error-boundary' 4 | import { useRemoteData, preloadRemote } from './use-remote-data' 5 | 6 | const Demo = () => { 7 | const { data } = useRemoteData() 8 | return
{data}
9 | } 10 | preloadRemote() 11 | 12 | function Fallback({ resetErrorBoundary }: any) { 13 | return ( 14 |
15 |

Something went wrong:

16 | 23 |
24 | ) 25 | } 26 | 27 | function RemoteData() { 28 | return ( 29 |
30 | { 33 | preloadRemote() 34 | }} 35 | > 36 | loading
}> 37 | 38 | 39 | 40 | 41 | ) 42 | } 43 | 44 | export default RemoteData 45 | -------------------------------------------------------------------------------- /examples/suspense/pages/index.js: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react' 2 | import Link from 'next/link' 3 | import fetcher from '../libs/fetch' 4 | 5 | import useSWR from 'swr' 6 | 7 | const Repos = ({ serverData }) => { 8 | const { data } = useSWR('/api/data', fetcher, { 9 | suspense: true, 10 | fallbackData: serverData 11 | }) 12 | 13 | return ( 14 | <> 15 | {data.map(project => ( 16 |

17 | 18 | {project} 19 | 20 |

21 | ))} 22 | 23 | ) 24 | } 25 | 26 | export default function Index({ serverData }) { 27 | return ( 28 |
29 |

Trending Projects

30 | loading...
}> 31 | 32 | 33 | 34 | ) 35 | } 36 | 37 | export const getServerSideProps = async () => { 38 | const data = await fetcher('http://localhost:3000/api/data') 39 | return { props: { serverData: data } } 40 | } 41 | -------------------------------------------------------------------------------- /src/subscription/types.ts: -------------------------------------------------------------------------------- 1 | import type { Key, SWRConfiguration, MutatorCallback } from '../index' 2 | 3 | export type SWRSubscriptionOptions = { 4 | next: (err?: Error | null, data?: Data | MutatorCallback) => void 5 | } 6 | 7 | export type SWRSubscription< 8 | SWRSubKey extends Key = Key, 9 | Data = any, 10 | Error = any 11 | > = SWRSubKey extends () => infer Arg | null | undefined | false 12 | ? (key: Arg, { next }: SWRSubscriptionOptions) => void 13 | : SWRSubKey extends null | undefined | false 14 | ? never 15 | : SWRSubKey extends infer Arg 16 | ? (key: Arg, { next }: SWRSubscriptionOptions) => void 17 | : never 18 | 19 | export type SWRSubscriptionResponse = { 20 | data?: Data 21 | error?: Error 22 | } 23 | 24 | export type SWRSubscriptionHook = < 25 | Data = any, 26 | Error = any, 27 | SWRSubKey extends Key = Key 28 | >( 29 | key: SWRSubKey, 30 | subscribe: SWRSubscription, 31 | config?: SWRConfiguration 32 | ) => SWRSubscriptionResponse 33 | -------------------------------------------------------------------------------- /e2e/site/app/suspense-after-preload/remote-data.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { Suspense, useState } from 'react' 3 | import useSWR from 'swr' 4 | import { preload } from 'swr' 5 | 6 | const fetcher = ([key, delay]: [key: string, delay: number]) => 7 | new Promise(r => { 8 | setTimeout(r, delay, key) 9 | }) 10 | 11 | const key = ['suspense-after-preload', 300] as const 12 | const useRemoteData = () => 13 | useSWR(key, fetcher, { 14 | suspense: true 15 | }) 16 | 17 | const Demo = () => { 18 | const { data } = useRemoteData() 19 | return
{data}
20 | } 21 | 22 | function Comp() { 23 | const [show, toggle] = useState(false) 24 | 25 | return ( 26 |
27 | 35 | {show ? ( 36 | loading
}> 37 | 38 | 39 | ) : null} 40 | 41 | ) 42 | } 43 | 44 | export default Comp 45 | -------------------------------------------------------------------------------- /examples/suspense-global/pages/api/data.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next' 2 | 3 | const projects = [ 4 | 'facebook/flipper', 5 | 'vuejs/vuepress', 6 | 'rust-lang/rust', 7 | 'vercel/next.js', 8 | 'emperor/clothes' 9 | ] as const 10 | 11 | export type ProjectsData = typeof projects 12 | 13 | export interface RepoData { 14 | forks_count: number 15 | stargazers_count: number 16 | watchers: number 17 | } 18 | 19 | export default function api(req: NextApiRequest, res: NextApiResponse) { 20 | if (req.query.id) { 21 | if (req.query.id === projects[4]) { 22 | setTimeout(() => { 23 | res.status(404).json({ msg: 'not found' }) 24 | }) 25 | } else { 26 | // a slow endpoint for getting repo data 27 | fetch(`https://api.github.com/repos/${req.query.id}`) 28 | .then(res => res.json()) 29 | .then(data => { 30 | setTimeout(() => { 31 | res.json(data) 32 | }, 2000) 33 | }) 34 | } 35 | } else { 36 | setTimeout(() => { 37 | res.json(projects) 38 | }, 2000) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/_internal/utils/resolve-args.ts: -------------------------------------------------------------------------------- 1 | import { mergeConfigs } from './merge-config' 2 | import { normalize } from './normalize-args' 3 | import { useSWRConfig } from './use-swr-config' 4 | import { BUILT_IN_MIDDLEWARE } from './middleware-preset' 5 | 6 | // It's tricky to pass generic types as parameters, so we just directly override 7 | // the types here. 8 | export const withArgs = (hook: any) => { 9 | return function useSWRArgs(...args: any) { 10 | // Get the default and inherited configuration. 11 | const fallbackConfig = useSWRConfig() 12 | 13 | // Normalize arguments. 14 | const [key, fn, _config] = normalize(args) 15 | 16 | // Merge configurations. 17 | const config = mergeConfigs(fallbackConfig, _config) 18 | 19 | // Apply middleware 20 | let next = hook 21 | const { use } = config 22 | const middleware = (use || []).concat(BUILT_IN_MIDDLEWARE) 23 | for (let i = middleware.length; i--; ) { 24 | next = middleware[i](next) 25 | } 26 | 27 | return next(key, fn || config.fetcher || null, config) 28 | } as unknown as SWRType 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Vercel, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/refetch-interval/pages/index.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import Button from '../components/button' 3 | import fetch from '../libs/fetch' 4 | 5 | import useSWR from 'swr' 6 | 7 | export default function Index() { 8 | const { data, mutate } = useSWR('/api/data', fetch, { 9 | // revalidate the data per second 10 | refreshInterval: 1000 11 | }) 12 | const [value, setValue] = useState('') 13 | 14 | if (!data) return

loading...

15 | 16 | return ( 17 |
18 |

Refetch Interval (1s)

19 |

Todo List

20 |
{ 21 | ev.preventDefault() 22 | setValue('') 23 | await fetch(`/api/data?add=${value}`) 24 | mutate() 25 | }}> 26 | setValue(ev.target.value)} /> 27 |
28 |
    29 | {data.map(item =>
  • {item}
  • )} 30 |
31 | 35 |
36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /examples/subscription/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import useSWRSubscription from "swr/subscription" 3 | import EventEmitter from "events" 4 | 5 | const event = new EventEmitter() 6 | 7 | // Simulating an external data source. 8 | let stopped = false 9 | async function start () { 10 | for (let i = 0; i < 100; i++) { 11 | await new Promise(res => setTimeout(res, 1000)) 12 | if (stopped) return 13 | if (i % 3 === 0 && i !== 0) { 14 | event.emit("error", new Error("error: " + i)); 15 | } else { 16 | event.emit("data", "state: " + i); 17 | } 18 | } 19 | } 20 | 21 | export default function page() { 22 | const { data, error } = useSWRSubscription('my-sub', (key, { next }) => { 23 | event.on("data", (value) => next(undefined, value)); 24 | event.on("error", (err) => next(err)); 25 | start(); 26 | return () => { 27 | stopped = true; 28 | }; 29 | }) 30 | 31 | return ( 32 |
33 |

SWR Subscription

34 |

Received every second, error when data is times of 3

35 |
{data}
36 |
{error ? error.message : ""}
37 |
38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /examples/server-render/pages/[pokemon].js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import fetcher from '../libs/fetcher' 3 | 4 | import useSWR from 'swr' 5 | 6 | const getURL = pokemon => `https://pokeapi.co/api/v2/pokemon/${pokemon}` 7 | 8 | export default function Pokemon({ pokemon, fallbackData }) { 9 | const { data } = useSWR(getURL(pokemon), fetcher, { fallbackData }) 10 | 11 | return ( 12 |
13 |

{pokemon}

14 | {data ? ( 15 |
16 |
17 | 18 |
19 |

height: {data.height}

20 |

weight: {data.weight}

21 |
    22 | {data.types.map(({ type }) => ( 23 |
  • {type.name}
  • 24 | ))} 25 |
26 |
27 | ) : ( 28 | 'loading...' 29 | )} 30 |
31 |
32 | 33 | Back 34 | 35 |
36 | ) 37 | } 38 | 39 | export async function getServerSideProps({ query }) { 40 | const data = await fetcher(getURL(query.pokemon)) 41 | return { props: { fallbackData: data, pokemon: query.pokemon } } 42 | } -------------------------------------------------------------------------------- /examples/prefetch-preload/README.md: -------------------------------------------------------------------------------- 1 | # Prefetch & Preload 2 | 3 | ## One-Click Deploy 4 | 5 | Deploy your own SWR project with Vercel. 6 | 7 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/prefetch-preload) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/prefetch-preload 15 | cd prefetch-preload 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | This example shows multiple ways to prefetch data to be used by SWR later. 31 | 32 | - Use a `` to get the browser to load the data while rendering the HTML 33 | - If in a browser, run the fetch + mutate outside the component 34 | - After rendering use an effect in React to prefetch the next page's data 35 | - When the user moves the mouse over a link trigger a fetch + mutate for the next page 36 | 37 | In the real world you would not necessarily use all of them at the same time but one or more combined to give the best UX possible. 38 | -------------------------------------------------------------------------------- /src/_internal/index.ts: -------------------------------------------------------------------------------- 1 | import SWRConfig from './utils/config-context' 2 | import * as revalidateEvents from './events' 3 | import { INFINITE_PREFIX } from './constants' 4 | 5 | export { SWRConfig, revalidateEvents, INFINITE_PREFIX } 6 | 7 | export { initCache } from './utils/cache' 8 | export { defaultConfig, cache, mutate, compare } from './utils/config' 9 | import { setupDevTools } from './utils/devtools' 10 | export * from './utils/env' 11 | export { SWRGlobalState } from './utils/global-state' 12 | export { stableHash } from './utils/hash' 13 | export * from './utils/helper' 14 | export * from './utils/shared' 15 | export { mergeConfigs } from './utils/merge-config' 16 | export { internalMutate } from './utils/mutate' 17 | export { normalize } from './utils/normalize-args' 18 | export { withArgs } from './utils/resolve-args' 19 | export { serialize } from './utils/serialize' 20 | export { subscribeCallback } from './utils/subscribe-key' 21 | export { getTimestamp } from './utils/timestamp' 22 | export { useSWRConfig } from './utils/use-swr-config' 23 | export { preset, defaultConfigOptions } from './utils/web-preset' 24 | export { withMiddleware } from './utils/with-middleware' 25 | export { preload } from './utils/preload' 26 | 27 | export * from './types' 28 | 29 | setupDevTools() 30 | -------------------------------------------------------------------------------- /test/type/preload.ts: -------------------------------------------------------------------------------- 1 | import { preload } from 'swr' 2 | import { expectType } from './utils' 3 | import type { Equal } from '@type-challenges/utils' 4 | 5 | export function testPreload() { 6 | const data1 = preload('key', () => Promise.resolve('value' as const)) 7 | expectType, typeof data1>>(true) 8 | 9 | const data2 = preload( 10 | () => 'key', 11 | () => 'value' as const 12 | ) 13 | expectType>(true) 14 | 15 | const data3 = preload<'value'>( 16 | () => 'key', 17 | () => 'value' as const 18 | ) 19 | // specifing a generic param breaks the rest type inference so get FetcherResponse<"value"> 20 | expectType, typeof data3>>(true) 21 | 22 | preload('key', key => { 23 | expectType>(true) 24 | }) 25 | 26 | preload<'value'>( 27 | 'key', 28 | ( 29 | // @ts-expect-error -- infered any implicitly 30 | key 31 | ) => { 32 | return 'value' as const 33 | } 34 | ) 35 | 36 | preload(['key', 1], keys => { 37 | expectType>(true) 38 | }) 39 | 40 | preload( 41 | () => 'key' as const, 42 | key => { 43 | expectType>(true) 44 | } 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /test/use-swr-devtools.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen } from '@testing-library/react' 2 | import React from 'react' 3 | 4 | describe('useSWR - devtools', () => { 5 | let useSWR, createKey, createResponse, renderWithConfig 6 | beforeEach(() => { 7 | const middleware = 8 | useSWRNext => 9 | (...args) => { 10 | const result = useSWRNext(...args) 11 | return { ...result, data: 'middleware' } 12 | } 13 | // @ts-expect-error 14 | window.__SWR_DEVTOOLS_USE__ = [middleware] 15 | // eslint-disable-next-line @typescript-eslint/no-require-imports 16 | ;({ createKey, createResponse, renderWithConfig } = require('./utils')) 17 | // eslint-disable-next-line @typescript-eslint/no-require-imports 18 | useSWR = require('swr').default 19 | }) 20 | it('window.__SWR_DEVTOOLS_USE__ should be set as middleware', async () => { 21 | const key = createKey() 22 | function Page() { 23 | const { data } = useSWR(key, () => createResponse('ok')) 24 | return
data: {data}
25 | } 26 | renderWithConfig() 27 | await screen.findByText('data: middleware') 28 | }) 29 | it('window.__SWR_DEVTOOLS_REACT__ should be the same reference with React', () => { 30 | // @ts-expect-error 31 | expect(window.__SWR_DEVTOOLS_REACT__).toBe(React) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /e2e/site/component/manual-retry-mutate.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react' 2 | import { ErrorBoundary } from 'react-error-boundary' 3 | import useSWR from 'swr' 4 | import { mutate } from 'swr' 5 | 6 | let count = 0 7 | export const fetcher = () => { 8 | count++ 9 | if (count === 1) return Promise.reject('wrong') 10 | return fetch('/api/retry') 11 | .then(r => r.json()) 12 | .then(r => r.name) 13 | } 14 | 15 | const key = 'manual-retry-mutate' 16 | 17 | export const useRemoteData = () => 18 | useSWR(key, fetcher, { 19 | suspense: true 20 | }) 21 | const Demo = () => { 22 | const { data } = useRemoteData() 23 | return
data: {data}
24 | } 25 | 26 | function Fallback({ resetErrorBoundary }: any) { 27 | return ( 28 |
29 |

Something went wrong

30 | 38 |
39 | ) 40 | } 41 | 42 | function RemoteData() { 43 | return ( 44 |
45 | 46 | loading
}> 47 | 48 | 49 | 50 | 51 | ) 52 | } 53 | 54 | export default RemoteData 55 | -------------------------------------------------------------------------------- /test/use-swr-node-env.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | // Do not lint the return value destruction for `renderToString` 6 | /* eslint-disable testing-library/render-result-naming-convention */ 7 | 8 | import { renderToString } from 'react-dom/server' 9 | import useSWR from 'swr' 10 | import useSWRImmutable from 'swr/immutable' 11 | import { IS_SERVER } from 'swr/_internal' 12 | import { createKey } from './utils' 13 | 14 | describe('useSWR', () => { 15 | it('env IS_SERVER is true in node env', () => { 16 | expect(IS_SERVER).toBe(true) 17 | }) 18 | 19 | it('should render fallback if provided on server side', async () => { 20 | const key = createKey() 21 | const useData = () => useSWR(key, k => k, { fallbackData: 'fallback' }) 22 | 23 | function Page() { 24 | const { data } = useData() 25 | return

{data}

26 | } 27 | 28 | const html = renderToString() 29 | expect(html).toContain('fallback') 30 | }) 31 | 32 | it('should not revalidate useSWRImmutable on server side', async () => { 33 | const key = createKey() 34 | const useData = () => useSWRImmutable(key, k => k) 35 | 36 | function Page() { 37 | const { data } = useData() 38 | return

{data || 'empty'}

39 | } 40 | 41 | const html = renderToString() 42 | expect(html).toContain('empty') 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /examples/suspense/pages/[user]/[repo].js: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react' 2 | import Link from 'next/link' 3 | import fetcher from '../../libs/fetch' 4 | import ErrorHandling from '../../components/error-handling' 5 | import useSWR from 'swr' 6 | 7 | const Detail = ({ id, serverData }) => { 8 | const { data } = useSWR('/api/data?id=' + id, fetcher, { suspense: true, fallbackData: serverData }) 9 | 10 | return ( 11 | <> 12 | {data ? ( 13 |
14 |

forks: {data.forks_count}

15 |

stars: {data.stargazers_count}

16 |

watchers: {data.watchers}

17 |
18 | ) : null} 19 | 20 | ) 21 | } 22 | 23 | export default function Repo({ id, serverData }) { 24 | return ( 25 |
26 |

{id}

27 | loading...
}> 28 | oooops!}> 29 | 30 | 31 | 32 |
33 |
34 | Back 35 | 36 | ) 37 | } 38 | 39 | export const getServerSideProps = async ({ params }) => { 40 | const { user, repo } = params 41 | const id = `${user}/${repo}` 42 | const data = await fetcher('http://localhost:3000/api/data?id=' + id).catch(() => {}) 43 | return { props: { serverData: data, id } } 44 | } -------------------------------------------------------------------------------- /src/_internal/utils/env.ts: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useLayoutEffect } from 'react' 2 | import { hasRequestAnimationFrame, isLegacyDeno, isWindowDefined } from './helper' 3 | 4 | export const IS_REACT_LEGACY = !React.useId 5 | 6 | export const IS_SERVER = !isWindowDefined || isLegacyDeno 7 | 8 | // Polyfill requestAnimationFrame 9 | export const rAF = ( 10 | f: (...args: any[]) => void 11 | ): number | ReturnType => 12 | hasRequestAnimationFrame() 13 | ? window['requestAnimationFrame'](f) 14 | : setTimeout(f, 1) 15 | 16 | // React currently throws a warning when using useLayoutEffect on the server. 17 | // To get around it, we can conditionally useEffect on the server (no-op) and 18 | // useLayoutEffect in the browser. 19 | export const useIsomorphicLayoutEffect = IS_SERVER ? useEffect : useLayoutEffect 20 | 21 | // This assignment is to extend the Navigator type to use effectiveType. 22 | const navigatorConnection = 23 | typeof navigator !== 'undefined' && 24 | ( 25 | navigator as Navigator & { 26 | connection?: { 27 | effectiveType: string 28 | saveData: boolean 29 | } 30 | } 31 | ).connection 32 | 33 | // Adjust the config based on slow connection status (<= 70Kbps). 34 | export const slowConnection = 35 | !IS_SERVER && 36 | navigatorConnection && 37 | (['slow-2g', '2g'].includes(navigatorConnection.effectiveType) || 38 | navigatorConnection.saveData) 39 | -------------------------------------------------------------------------------- /playwright.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test' 2 | 3 | export default defineConfig({ 4 | webServer: { 5 | command: 'pnpm next start e2e/site --port 4000', 6 | reuseExistingServer: !process.env.CI, 7 | port: 4000 8 | }, 9 | testDir: './e2e', 10 | /* The base directory, relative to the config file, for snapshot files created with toMatchSnapshot and toHaveScreenshot. */ 11 | snapshotDir: './e2e/__snapshots__', 12 | /* Maximum time one test can run for. */ 13 | timeout: 10 * 1000, 14 | /* Run tests in files in parallel */ 15 | fullyParallel: true, 16 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 17 | forbidOnly: !!process.env.CI, 18 | /* Retry on CI only */ 19 | retries: process.env.CI ? 2 : 0, 20 | /* Opt out of parallel tests on CI. */ 21 | workers: process.env.CI ? 1 : undefined, 22 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 23 | reporter: process.env.CI 24 | ? [['github'], ['html', { open: 'on-failure' }]] 25 | : [['html', { open: 'on-failure' }]], 26 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 27 | use: { 28 | baseURL: 'http://localhost:4000', 29 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 30 | trace: process.env.CI ? 'on-first-retry' : 'on', 31 | ...devices['Desktop Chrome'] 32 | } 33 | }) 34 | -------------------------------------------------------------------------------- /examples/local-state-sharing/pages/index.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | import initialStore from "../libs/store" 3 | import useSWR, { mutate } from "swr" 4 | 5 | function Profile() { 6 | const { data } = useSWR("globalState", { fallbackData: initialStore }) 7 | const [value, updateValue] = useState((data || {}).name) 8 | if (!data) { 9 | return null 10 | } 11 | return ( 12 |
13 |

My name is {data.name}.

14 | updateValue(e.target.value)} 17 | style={{ width: 200, marginRight: 8 }} 18 | /> 19 | 27 |
28 | ) 29 | } 30 | 31 | function Other() { 32 | const { data } = useSWR("globalState", { fallbackData: initialStore }) 33 | if (!data) { 34 | return null 35 | } 36 | return ( 37 |
38 |

39 | Another Component:
40 | My name is {data.name}. 41 |

42 |
43 | ) 44 | } 45 | 46 | export default function Index() { 47 | return ( 48 |
49 | useSWR can share state between components: 50 | 51 | 52 |
53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /scripts/bump-next-version.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const { execSync } = require('child_process') 4 | const semver = require('semver') 5 | 6 | const packageJsonPath = path.join(__dirname, '../package.json') 7 | const packageJsonData = fs.readFileSync(packageJsonPath, 'utf8') 8 | const packageJson = JSON.parse(packageJsonData) 9 | 10 | let version = packageJson.version 11 | const releaseType = process.env.RELEASE_TYPE || 'beta' 12 | const semverType = process.env.SEMVER_TYPE 13 | 14 | function bumpVersion(version) { 15 | if (process.env.DRY_RUN) { 16 | console.log(`npm version ${version}`) 17 | } else { 18 | try { 19 | execSync(`npm version ${version}`, { stdio: 'inherit' }) 20 | } catch (error) { 21 | console.error('Failed to execute npm version:', error) 22 | process.exit(1) 23 | } 24 | } 25 | } 26 | 27 | if (releaseType === 'beta') { 28 | if (semver.prerelease(version)) { 29 | version = semver.inc(version, 'prerelease') 30 | } else { 31 | version = semver.inc(version, 'pre' + semverType, 'beta') 32 | } 33 | } else if (releaseType === 'stable') { 34 | if (!semverType) { 35 | console.error('Missing semver type. Expected "patch", "minor" or "major".') 36 | process.exit(1) 37 | } 38 | version = semver.inc(version, semverType) 39 | } else { 40 | console.error('Invalid release type. Expected "beta" or "stable".') 41 | process.exit(1) 42 | } 43 | 44 | bumpVersion(version) 45 | -------------------------------------------------------------------------------- /.github/workflows/trigger-release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | inputs: 4 | releaseType: 5 | description: Release stable or beta? 6 | required: true 7 | type: choice 8 | options: 9 | - beta 10 | - stable 11 | 12 | semverType: 13 | description: semver type? 14 | type: choice 15 | options: 16 | - patch 17 | - minor 18 | - major 19 | 20 | name: Trigger Release 21 | 22 | env: 23 | SEMVER_TYPE: ${{ github.event.inputs.semverType }} 24 | RELEASE_TYPE: ${{ github.event.inputs.releaseType }} 25 | 26 | jobs: 27 | start: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v4 32 | with: 33 | fetch-depth: 10 34 | token: ${{ secrets.RELEASE_BOT_GITHUB_TOKEN }} 35 | - name: Install 36 | uses: ./.github/workflows/install 37 | - name: Test 38 | run: | 39 | pnpm clean 40 | pnpm build 41 | pnpm run-all-checks 42 | pnpm test:build 43 | 44 | - name: Configure git 45 | run: | 46 | git config user.name "vercel-release-bot" 47 | git config user.email "infra+release@vercel.com" 48 | 49 | - name: Bump version and tag 50 | run: | 51 | node ./scripts/bump-next-version.js 52 | 53 | - name: Git push 54 | run: | 55 | git push origin main 56 | git push origin --tags 57 | -------------------------------------------------------------------------------- /examples/autocomplete-suggestions/pages/index.js: -------------------------------------------------------------------------------- 1 | import { useMemo, useState } from "react" 2 | import fetcher from '../libs/fetcher' 3 | import { 4 | Combobox, 5 | ComboboxInput, 6 | ComboboxPopover, 7 | ComboboxList, 8 | ComboboxOption 9 | } from '@reach/combobox' 10 | import debounce from 'lodash.debounce' 11 | 12 | import useSWR from 'swr' 13 | 14 | export default function Index() { 15 | const [searchTerm, setSearchTerm] = useState(null) 16 | const { data: countries = [], isValidating } = useSWR( 17 | () => (searchTerm ? `/api/suggestions?value=${searchTerm}` : null), 18 | fetcher 19 | ) 20 | 21 | function handleChange(event) { 22 | setSearchTerm(event.target.value) 23 | } 24 | 25 | const debouncedHandleChange = useMemo( 26 | () => debounce(handleChange, 500) 27 | , []); 28 | 29 | return ( 30 |
31 |

Country Search

32 | 33 | 38 | {countries && countries.length > 0 && ( 39 | 40 | 41 | {countries.map(country => ( 42 | 43 | ))} 44 | 45 | 46 | )} 47 | 48 |
49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /examples/prefetch-preload/pages/[user]/[repo].js: -------------------------------------------------------------------------------- 1 | import Head from "next/head" 2 | import Link from 'next/link' 3 | import React from 'react' 4 | import fetch from '../../libs/fetch' 5 | 6 | import useSWR, { mutate } from 'swr' 7 | 8 | function prefetchParent() { 9 | return fetch('/api/data') 10 | .then(projects => mutate('/api/data', projects, false)) 11 | } 12 | 13 | // if we are on the browser trigger a prefetch as soon as possible 14 | if (typeof window !== 'undefined') prefetchParent() 15 | 16 | export default function Repo() { 17 | const id = typeof window !== 'undefined' ? window.location.pathname.slice(1) : '' 18 | const { data } = useSWR('/api/data?id=' + id, fetch) 19 | 20 | React.useEffect(() => { 21 | prefetchParent() 22 | }, []) 23 | 24 | function handleMouseEnter() { 25 | prefetchParent() 26 | } 27 | 28 | return ( 29 | <> 30 | 31 | {/* This will tell the browser to preload the data for our page */} 32 | {id && } 33 | 34 |
35 |

{id}

36 | { 37 | data ?
38 |

forks: {data.forks_count}

39 |

stars: {data.stargazers_count}

40 |

watchers: {data.watchers}

41 |
: 'loading...' 42 | } 43 |
44 |
45 | Back 46 |
47 | 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /e2e/test/concurrent-transition.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test' 2 | 3 | test.describe('concurrent rendering transitions', () => { 4 | test('should pause when changing the key inside a transition', async ({ 5 | page 6 | }) => { 7 | // Navigate to the test page 8 | await page.goto('./concurrent-transition', { waitUntil: 'networkidle' }) 9 | 10 | // Wait for page to be fully loaded and interactive 11 | await expect(page.getByTestId('pending-state')).toContainText('isPending:0') 12 | 13 | // Wait for initial data to load 14 | await expect(page.getByTestId('data-content')).toContainText( 15 | 'data:initial-key' 16 | ) 17 | 18 | // Ensure the component is in a stable state before triggering transition 19 | await page.waitForTimeout(100) 20 | 21 | // Click to trigger transition 22 | await page.getByTestId('transition-trigger').click() 23 | 24 | // Verify transition starts - isPending becomes true 25 | await expect(page.getByTestId('pending-state')).toContainText('isPending:1') 26 | 27 | // During transition: data should still show old value (this is the key behavior) 28 | // In React 19, this behavior should be more consistent 29 | await expect(page.getByTestId('data-content')).toContainText( 30 | 'data:initial-key' 31 | ) 32 | 33 | // Wait for transition to complete 34 | await expect(page.getByTestId('pending-state')).toContainText('isPending:0') 35 | await expect(page.getByTestId('data-content')).toContainText('data:new-key') 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /e2e/site/app/perf/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useState } from 'react' 3 | import useSWR from 'swr' 4 | 5 | const elementCount = 10_000 6 | const useData = () => { 7 | return useSWR('1', async (url: string) => { 8 | return 1 9 | }) 10 | } 11 | 12 | const HookUser = () => { 13 | const { data } = useData() 14 | return
{data}
15 | } 16 | /** 17 | * This renders 10,000 divs and is used to compare against the render performance 18 | * when using swr. 19 | */ 20 | const CheapComponent = () => { 21 | const cheapComponents = Array.from({ length: elementCount }, (_, i) => ( 22 |
{i}
23 | )) 24 | return ( 25 |
26 |

Cheap Component

27 | {cheapComponents} 28 |
29 | ) 30 | } 31 | 32 | /** 33 | * This renders 10,000 divs, each of which uses the same swr hook. 34 | */ 35 | const ExpensiveComponent = () => { 36 | const hookComponents = Array.from({ length: elementCount }, (_, i) => ( 37 | 38 | )) 39 | return ( 40 |
41 |

Expensive Component

42 | {hookComponents} 43 |
44 | ) 45 | } 46 | 47 | export default function PerformancePage() { 48 | const [renderExpensive, setRenderExpensive] = useState(false) 49 | return ( 50 |
51 |

Performance Page

52 | 60 | {!renderExpensive ? : } 61 |
62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /examples/optimistic-ui/styles.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, 3 | Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 4 | text-align: center; 5 | } 6 | 7 | body { 8 | max-width: 600px; 9 | margin: auto; 10 | } 11 | 12 | h1 { 13 | margin-top: 1em; 14 | } 15 | 16 | .note { 17 | text-align: left; 18 | font-size: 0.9em; 19 | line-height: 1.5; 20 | color: #666; 21 | } 22 | 23 | .note svg { 24 | margin-right: 0.5em; 25 | vertical-align: -2px; 26 | width: 14px; 27 | height: 14px; 28 | margin-right: 5px; 29 | } 30 | 31 | form { 32 | display: flex; 33 | margin: 8px 0; 34 | gap: 8px; 35 | } 36 | 37 | input { 38 | flex: 1; 39 | } 40 | 41 | input, 42 | button { 43 | font-size: 16px; 44 | padding: 6px 5px; 45 | } 46 | 47 | code { 48 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 49 | Liberation Mono, Courier New, monospace; 50 | font-feature-settings: 'rlig' 1, 'calt' 1, 'ss01' 1; 51 | background-color: #eee; 52 | padding: 1px 3px; 53 | border-radius: 2px; 54 | } 55 | 56 | ul { 57 | text-align: left; 58 | list-style: none; 59 | padding: 0; 60 | } 61 | 62 | li { 63 | margin: 8px 0; 64 | padding: 10px; 65 | border-radius: 4px; 66 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12), 0 0 0 1px #ededed; 67 | } 68 | 69 | i { 70 | color: #999; 71 | } 72 | 73 | .info, 74 | .success, 75 | .error { 76 | display: block; 77 | text-align: left; 78 | padding: 6px 0; 79 | font-size: 0.9em; 80 | opacity: 0.9; 81 | } 82 | 83 | .info { 84 | color: #666; 85 | } 86 | .success { 87 | color: #4caf50; 88 | } 89 | .error { 90 | color: #f44336; 91 | } 92 | -------------------------------------------------------------------------------- /examples/optimistic-ui-immer/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import fetch from '../libs/fetch' 3 | 4 | import useSWR, { mutate } from 'swr' 5 | import produce from "immer" 6 | 7 | export default function Index() { 8 | const [text, setText] = React.useState(''); 9 | const { data } = useSWR('/api/data', fetch) 10 | 11 | async function handleSubmit(event) { 12 | event.preventDefault() 13 | // Call mutate to optimistically update the UI. 14 | // We use Immer produce to allow us to perform an immutable change 15 | // while coding it as a normal mutation of the same object. 16 | mutate("/api/data", produce(draftData => { 17 | draftData.push(text) 18 | }), false) 19 | // Then we send the request to the API and let mutate 20 | // update the data with the API response. 21 | // Our action may fail in the API function, and the response differ 22 | // from what was optimistically updated, in that case, the UI will be 23 | // changed to match the API response. 24 | // The fetch could also fail, in that case, the UI will 25 | // be in an incorrect state until the next successful fetch. 26 | mutate('/api/data', await fetch('/api/data', { 27 | method: 'POST', 28 | body: JSON.stringify({ text }) 29 | })) 30 | setText('') 31 | } 32 | 33 | return
34 |
35 | setText(event.target.value)} 38 | value={text} 39 | /> 40 | 41 |
42 |
    43 | {data ? data.map(datum =>
  • {datum}
  • ) : 'loading...'} 44 |
45 |
46 | } 47 | -------------------------------------------------------------------------------- /examples/axios-typescript/libs/useRequest.ts: -------------------------------------------------------------------------------- 1 | import useSWR, { SWRConfiguration, SWRResponse } from 'swr' 2 | import axios, { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios' 3 | 4 | export type GetRequest = AxiosRequestConfig | null 5 | 6 | interface Return 7 | extends Pick< 8 | SWRResponse, AxiosError>, 9 | 'isValidating' | 'error' | 'mutate' 10 | > { 11 | data: Data | undefined 12 | response: AxiosResponse | undefined 13 | } 14 | 15 | export interface Config 16 | extends Omit< 17 | SWRConfiguration, AxiosError>, 18 | 'fallbackData' 19 | > { 20 | fallbackData?: Data 21 | } 22 | 23 | export default function useRequest( 24 | request: GetRequest, 25 | { fallbackData, ...config }: Config = {} 26 | ): Return { 27 | const { 28 | data: response, 29 | error, 30 | isValidating, 31 | mutate 32 | } = useSWR, AxiosError>( 33 | request, 34 | /** 35 | * NOTE: Typescript thinks `request` can be `null` here, but the fetcher 36 | * function is actually only called by `useSWR` when it isn't. 37 | */ 38 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 39 | () => axios.request(request!), 40 | { 41 | ...config, 42 | fallbackData: fallbackData && { 43 | status: 200, 44 | statusText: 'InitialData', 45 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 46 | config: request!, 47 | headers: {}, 48 | data: fallbackData 49 | } as AxiosResponse 50 | } 51 | ) 52 | 53 | return { 54 | data: response && response.data, 55 | response, 56 | error, 57 | isValidating, 58 | mutate 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /e2e/site/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 18 | 19 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 20 | 21 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 22 | 23 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 24 | 25 | ## Learn More 26 | 27 | To learn more about Next.js, take a look at the following resources: 28 | 29 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 30 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 31 | 32 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 33 | 34 | ## Deploy on Vercel 35 | 36 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 37 | 38 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 39 | -------------------------------------------------------------------------------- /e2e/test/stream-ssr.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test' 2 | 3 | test.describe('Stream SSR', () => { 4 | test('Basic SSR', async ({ page }) => { 5 | const log: any[] = [] 6 | await page.exposeFunction('consoleError', (msg: any) => log.push(msg)) 7 | await page.addInitScript(` 8 | const onError = window.onerror 9 | window.onerror = (...args) => { 10 | consoleError(...args) 11 | onError(...args) 12 | } 13 | `) 14 | await page.goto('./basic-ssr', { waitUntil: 'commit' }) 15 | await expect(page.getByText('result:undefined')).toBeVisible() 16 | await expect(page.getByText('result:SSR Works')).toBeVisible() 17 | await expect(page.getByText('history:[null,"SSR Works"]')).toBeVisible() 18 | expect(log).toHaveLength(0) 19 | }) 20 | 21 | test('Partially Hydrate', async ({ page }) => { 22 | const log: any[] = [] 23 | await page.exposeFunction('consoleError', (msg: any) => log.push(msg)) 24 | await page.addInitScript(` 25 | const onError = window.onerror 26 | window.onerror = (...args) => { 27 | consoleError(...args) 28 | onError(...args) 29 | } 30 | `) 31 | await page.goto('./partially-hydrate', { waitUntil: 'commit' }) 32 | await expect(page.getByText('first data:undefined')).toBeVisible() 33 | await expect( 34 | page.getByText('second data (delayed hydration):undefined') 35 | ).toBeVisible() 36 | await expect(page.getByText('first data:SSR Works')).toBeVisible() 37 | await expect( 38 | page.getByText('second data (delayed hydration):SSR Works') 39 | ).toBeVisible() 40 | await expect( 41 | page.getByText('first history:[null,"SSR Works"]') 42 | ).toBeVisible() 43 | await expect( 44 | page.getByText('second history:[null,"SSR Works"]') 45 | ).toBeVisible() 46 | expect(log).toHaveLength(0) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /test/use-swr-offline.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen } from '@testing-library/react' 2 | import { act } from 'react' 3 | import useSWR from 'swr' 4 | import { 5 | nextTick as waitForNextTick, 6 | focusOn, 7 | createKey, 8 | renderWithConfig 9 | } from './utils' 10 | 11 | const focusWindow = () => focusOn(window) 12 | const dispatchWindowEvent = event => 13 | act(async () => { 14 | window.dispatchEvent(new Event(event)) 15 | }) 16 | 17 | describe('useSWR - offline', () => { 18 | it('should not revalidate when offline', async () => { 19 | let value = 0 20 | 21 | const key = createKey() 22 | function Page() { 23 | const { data } = useSWR(key, () => value++, { 24 | dedupingInterval: 0 25 | }) 26 | return
data: {data}
27 | } 28 | 29 | renderWithConfig() 30 | // hydration 31 | screen.getByText('data:') 32 | // mount 33 | await screen.findByText('data: 0') 34 | 35 | // simulate offline 36 | await waitForNextTick() 37 | await dispatchWindowEvent('offline') 38 | 39 | // trigger focus revalidation 40 | await focusWindow() 41 | 42 | // should not be revalidated 43 | screen.getByText('data: 0') 44 | }) 45 | 46 | it('should revalidate immediately when becoming online', async () => { 47 | let value = 0 48 | 49 | const key = createKey() 50 | function Page() { 51 | const { data } = useSWR(key, () => value++, { 52 | dedupingInterval: 0 53 | }) 54 | return
data: {data}
55 | } 56 | 57 | renderWithConfig() 58 | // hydration 59 | screen.getByText('data:') 60 | // mount 61 | await screen.findByText('data: 0') 62 | 63 | // simulate online 64 | await waitForNextTick() 65 | await dispatchWindowEvent('online') 66 | 67 | // should be revalidated 68 | await screen.findByText('data: 1') 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /e2e/site/app/concurrent-transition/transition-demo.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React, { useState, useTransition, Suspense, useCallback } from 'react' 4 | import useSWR from 'swr' 5 | 6 | // Simulate API data fetching with delay 7 | const fetcher = async (key: string): Promise => { 8 | // Slightly longer delay to make transition behavior more observable 9 | await new Promise(resolve => setTimeout(resolve, 150)) 10 | return key 11 | } 12 | 13 | // Component that uses SWR with suspense 14 | function DataComponent({ swrKey }: { swrKey: string }) { 15 | const { data } = useSWR(swrKey, fetcher, { 16 | dedupingInterval: 0, 17 | suspense: true, 18 | // React 19 improvements for concurrent features 19 | keepPreviousData: false 20 | }) 21 | 22 | return data:{data} 23 | } 24 | 25 | export default function TransitionDemo() { 26 | const [isPending, startTransition] = useTransition() 27 | const [key, setKey] = useState('initial-key') 28 | 29 | const handleTransition = useCallback(() => { 30 | startTransition(() => { 31 | setKey('new-key') 32 | }) 33 | }, []) 34 | 35 | return ( 36 |
37 |

React 19 Concurrent Transition Demo

38 |
49 |
isPending:{isPending ? '1' : '0'}
50 | loading} 52 | > 53 | 54 | 55 |

56 | Click to test concurrent transition behavior 57 |

58 |
59 |
60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /e2e/test/initial-render.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test' 2 | 3 | test.describe('rendering', () => { 4 | test('suspense with preload', async ({ page }) => { 5 | await page.goto('./suspense-after-preload', { waitUntil: 'commit' }) 6 | await page.getByRole('button', { name: 'preload' }).click() 7 | await expect(page.getByText('suspense-after-preload')).toBeVisible() 8 | }) 9 | 10 | test('should be able to retry in suspense with react 19 (app router)', async ({ 11 | page 12 | }) => { 13 | await page.goto('./suspense-retry', { waitUntil: 'commit' }) 14 | await expect(page.getByText('Something went wrong')).toBeVisible() 15 | await page.getByRole('button', { name: 'retry' }).click() 16 | await expect(page.getByText('data: SWR suspense retry works')).toBeVisible() 17 | }) 18 | 19 | test('should be able to retry in suspense with react 19 (pages router)', async ({ 20 | page 21 | }) => { 22 | await page.goto('./suspense-retry-19', { waitUntil: 'commit' }) 23 | await expect(page.getByText('Something went wrong')).toBeVisible() 24 | await page.getByRole('button', { name: 'retry' }).click() 25 | await expect(page.getByText('data: SWR suspense retry works')).toBeVisible() 26 | }) 27 | 28 | test('should be able to retry in suspense with mutate', async ({ page }) => { 29 | await page.goto('./suspense-retry-mutate', { waitUntil: 'commit' }) 30 | await expect(page.getByText('Something went wrong')).toBeVisible() 31 | await page.getByRole('button', { name: 'retry' }).click() 32 | await expect(page.getByText('data: SWR suspense retry works')).toBeVisible() 33 | }) 34 | 35 | test('should be able to use `unstable_serialize` in server component', async ({ 36 | page 37 | }) => { 38 | await page.goto('./react-server-entry', { waitUntil: 'commit' }) 39 | await expect(page.getByText('unstable_serialize: useSWR')).toBeVisible() 40 | await expect( 41 | page.getByText('infinite_unstable_serialize: $inf$useSWRInfinite') 42 | ).toBeVisible() 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /src/_internal/utils/helper.ts: -------------------------------------------------------------------------------- 1 | import type { Cache, State, GlobalState } from '../types' 2 | import { SWRGlobalState } from './global-state' 3 | import { isUndefined, mergeObjects } from './shared' 4 | 5 | const EMPTY_CACHE = {} 6 | const INITIAL_CACHE: Record = {} 7 | 8 | const STR_UNDEFINED = 'undefined' 9 | 10 | // NOTE: Use the function to guarantee it's re-evaluated between jsdom and node runtime for tests. 11 | export const isWindowDefined = typeof window != STR_UNDEFINED 12 | export const isDocumentDefined = typeof document != STR_UNDEFINED 13 | export const isLegacyDeno = isWindowDefined && 'Deno' in window 14 | 15 | export const hasRequestAnimationFrame = () => 16 | isWindowDefined && typeof window['requestAnimationFrame'] != STR_UNDEFINED 17 | 18 | export const createCacheHelper = >( 19 | cache: Cache, 20 | key: string | undefined 21 | ) => { 22 | const state = SWRGlobalState.get(cache) as GlobalState 23 | return [ 24 | // Getter 25 | () => ((!isUndefined(key) && cache.get(key)) || EMPTY_CACHE) as T, 26 | // Setter 27 | (info: T) => { 28 | if (!isUndefined(key)) { 29 | const prev = cache.get(key) 30 | 31 | // Before writing to the store, we keep the value in the initial cache 32 | // if it's not there yet. 33 | if (!(key in INITIAL_CACHE)) { 34 | INITIAL_CACHE[key] = prev 35 | } 36 | 37 | state[5](key, mergeObjects(prev, info), prev || EMPTY_CACHE) 38 | } 39 | }, 40 | // Subscriber 41 | state[6], 42 | // Get server cache snapshot 43 | () => { 44 | if (!isUndefined(key)) { 45 | // If the cache was updated on the client, we return the stored initial value. 46 | if (key in INITIAL_CACHE) return INITIAL_CACHE[key] 47 | } 48 | 49 | // If we haven't done any client-side updates, we return the current value. 50 | return ((!isUndefined(key) && cache.get(key)) || EMPTY_CACHE) as T 51 | } 52 | ] as const 53 | } 54 | 55 | // export { UNDEFINED, OBJECT, isUndefined, isFunction, mergeObjects, isPromiseLike } 56 | -------------------------------------------------------------------------------- /src/_internal/utils/preload.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Middleware, 3 | Key, 4 | BareFetcher, 5 | GlobalState, 6 | FetcherResponse 7 | } from '../types' 8 | import { serialize } from './serialize' 9 | import { cache } from './config' 10 | import { SWRGlobalState } from './global-state' 11 | import { isUndefined } from './shared' 12 | import { INFINITE_PREFIX } from '../constants' 13 | // Basically same as Fetcher but without Conditional Fetching 14 | type PreloadFetcher< 15 | Data = unknown, 16 | SWRKey extends Key = Key 17 | > = SWRKey extends () => infer Arg 18 | ? (arg: Arg) => FetcherResponse 19 | : SWRKey extends infer Arg 20 | ? (arg: Arg) => FetcherResponse 21 | : never 22 | 23 | export const preload = < 24 | Data = any, 25 | SWRKey extends Key = Key, 26 | Fetcher extends BareFetcher = PreloadFetcher 27 | >( 28 | key_: SWRKey, 29 | fetcher: Fetcher 30 | ): ReturnType => { 31 | const [key, fnArg] = serialize(key_) 32 | const [, , , PRELOAD] = SWRGlobalState.get(cache) as GlobalState 33 | 34 | // Prevent preload to be called multiple times before used. 35 | if (PRELOAD[key]) return PRELOAD[key] 36 | 37 | const req = fetcher(fnArg) as ReturnType 38 | PRELOAD[key] = req 39 | return req 40 | } 41 | 42 | export const middleware: Middleware = 43 | useSWRNext => (key_, fetcher_, config) => { 44 | // fetcher might be a sync function, so this should not be an async function 45 | const fetcher = 46 | fetcher_ && 47 | ((...args: any[]) => { 48 | const [key] = serialize(key_) 49 | const [, , , PRELOAD] = SWRGlobalState.get(cache) as GlobalState 50 | 51 | if (key.startsWith(INFINITE_PREFIX)) { 52 | // we want the infinite fetcher to be called. 53 | // handling of the PRELOAD cache happens there. 54 | return fetcher_(...args) 55 | } 56 | 57 | const req = PRELOAD[key] 58 | if (isUndefined(req)) return fetcher_(...args) 59 | delete PRELOAD[key] 60 | return req 61 | }) 62 | return useSWRNext(key_, fetcher, config) 63 | } 64 | -------------------------------------------------------------------------------- /src/_internal/utils/config.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | PublicConfiguration, 3 | FullConfiguration, 4 | RevalidatorOptions, 5 | Revalidator, 6 | ScopedMutator, 7 | Cache 8 | } from '../types' 9 | 10 | import { initCache } from './cache' 11 | import { preset } from './web-preset' 12 | import { slowConnection } from './env' 13 | import { isUndefined, noop, mergeObjects } from './shared' 14 | 15 | import { dequal } from 'dequal/lite' 16 | 17 | // error retry 18 | const onErrorRetry = ( 19 | _: unknown, 20 | __: string, 21 | config: Readonly, 22 | revalidate: Revalidator, 23 | opts: Required 24 | ): void => { 25 | const maxRetryCount = config.errorRetryCount 26 | const currentRetryCount = opts.retryCount 27 | 28 | // Exponential backoff 29 | const timeout = 30 | ~~( 31 | (Math.random() + 0.5) * 32 | (1 << (currentRetryCount < 8 ? currentRetryCount : 8)) 33 | ) * config.errorRetryInterval 34 | 35 | if (!isUndefined(maxRetryCount) && currentRetryCount > maxRetryCount) { 36 | return 37 | } 38 | 39 | setTimeout(revalidate, timeout, opts) 40 | } 41 | 42 | const compare = dequal 43 | 44 | // Default cache provider 45 | const [cache, mutate] = initCache(new Map()) as [Cache, ScopedMutator] 46 | export { cache, mutate, compare } 47 | 48 | // Default config 49 | export const defaultConfig: FullConfiguration = mergeObjects( 50 | { 51 | // events 52 | onLoadingSlow: noop, 53 | onSuccess: noop, 54 | onError: noop, 55 | onErrorRetry, 56 | onDiscarded: noop, 57 | 58 | // switches 59 | revalidateOnFocus: true, 60 | revalidateOnReconnect: true, 61 | revalidateIfStale: true, 62 | shouldRetryOnError: true, 63 | 64 | // timeouts 65 | errorRetryInterval: slowConnection ? 10000 : 5000, 66 | focusThrottleInterval: 5 * 1000, 67 | dedupingInterval: 2 * 1000, 68 | loadingTimeout: slowConnection ? 5000 : 3000, 69 | 70 | // providers 71 | compare, 72 | isPaused: () => false, 73 | cache, 74 | mutate, 75 | fallback: {} 76 | }, 77 | // use web preset by default 78 | preset 79 | ) 80 | -------------------------------------------------------------------------------- /examples/prefetch-preload/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Head from "next/head"; 3 | import Link from 'next/link' 4 | import fetch from '../libs/fetch' 5 | 6 | import useSWR, { mutate } from 'swr' 7 | 8 | function prefetchData() { 9 | return fetch('/api/data') 10 | .then(data => { 11 | mutate('/api/data', data, false) 12 | return data 13 | }) 14 | } 15 | 16 | function prefetchItem(project) { 17 | return fetch(`https://api.github.com/repos/${project}`).then(data => { 18 | mutate(`/api/data?id=${project}`, data, false) 19 | return data 20 | }) 21 | } 22 | 23 | function prefetchWithProjects() { 24 | return prefetchData() 25 | .then(projects => Promise.all(projects.map(prefetchItem))) 26 | } 27 | 28 | // if we are on the browser trigger a prefetch as soon as possible 29 | if (typeof window !== 'undefined') prefetchWithProjects() 30 | 31 | export default function Index() { 32 | const { data } = useSWR('/api/data', fetch) 33 | 34 | // This effect will fetch all projects after mounting 35 | React.useEffect(() => { 36 | if (!data) return 37 | if (data.length === 0) return 38 | data.forEach(prefetchItem) 39 | }, [data]); 40 | 41 | // With this handler, you could prefetch the data for a specific 42 | // project the moment the user moves the mouse over the link 43 | function handleMouseEnter(event) { 44 | // In our case, we could get the ID from the href so we use that 45 | prefetchItem(event.target.getAttribute("href").slice(1)) 46 | } 47 | 48 | return ( 49 | <> 50 | 51 | {/* This will tell the browser to preload the data for our page */} 52 | 53 | 54 |
55 |

Trending Projects

56 |
57 | { 58 | data ? data.map(project => 59 |

60 | 61 | {project} 62 | 63 |

64 | ) : 'loading...' 65 | } 66 |
67 |
68 | 69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /src/_internal/utils/web-preset.ts: -------------------------------------------------------------------------------- 1 | import type { ProviderConfiguration } from '../types' 2 | import { isWindowDefined, isDocumentDefined } from './helper' 3 | import { isUndefined, noop } from './shared' 4 | 5 | /** 6 | * Due to the bug https://bugs.chromium.org/p/chromium/issues/detail?id=678075, 7 | * it's not reliable to detect if the browser is currently online or offline 8 | * based on `navigator.onLine`. 9 | * As a workaround, we always assume it's online on the first load, and change 10 | * the status upon `online` or `offline` events. 11 | */ 12 | let online = true 13 | const isOnline = () => online 14 | 15 | // For node and React Native, `add/removeEventListener` doesn't exist on window. 16 | const [onWindowEvent, offWindowEvent] = 17 | isWindowDefined && window.addEventListener 18 | ? [ 19 | window.addEventListener.bind(window), 20 | window.removeEventListener.bind(window) 21 | ] 22 | : [noop, noop] 23 | 24 | const isVisible = () => { 25 | const visibilityState = isDocumentDefined && document.visibilityState 26 | return isUndefined(visibilityState) || visibilityState !== 'hidden' 27 | } 28 | 29 | const initFocus = (callback: () => void) => { 30 | // focus revalidate 31 | if (isDocumentDefined) { 32 | document.addEventListener('visibilitychange', callback) 33 | } 34 | onWindowEvent('focus', callback) 35 | return () => { 36 | if (isDocumentDefined) { 37 | document.removeEventListener('visibilitychange', callback) 38 | } 39 | offWindowEvent('focus', callback) 40 | } 41 | } 42 | 43 | const initReconnect = (callback: () => void) => { 44 | // revalidate on reconnected 45 | const onOnline = () => { 46 | online = true 47 | callback() 48 | } 49 | // nothing to revalidate, just update the status 50 | const onOffline = () => { 51 | online = false 52 | } 53 | onWindowEvent('online', onOnline) 54 | onWindowEvent('offline', onOffline) 55 | return () => { 56 | offWindowEvent('online', onOnline) 57 | offWindowEvent('offline', onOffline) 58 | } 59 | } 60 | 61 | export const preset = { 62 | isOnline, 63 | isVisible 64 | } as const 65 | 66 | export const defaultConfigOptions: ProviderConfiguration = { 67 | initFocus, 68 | initReconnect 69 | } 70 | -------------------------------------------------------------------------------- /test/use-swr-concurrent-rendering.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen } from '@testing-library/react' 2 | import { 3 | createKey, 4 | createResponse, 5 | sleep, 6 | executeWithoutBatching, 7 | renderWithConfig 8 | } from './utils' 9 | import React, { act } from 'react' 10 | 11 | import useSWR from 'swr' 12 | 13 | describe('useSWR - concurrent rendering', () => { 14 | it('should fetch data in concurrent rendering', async () => { 15 | const key = createKey() 16 | function Page() { 17 | const { data } = useSWR(key, () => createResponse('0', { delay: 50 }), { 18 | dedupingInterval: 0 19 | }) 20 | return
data:{data}
21 | } 22 | 23 | renderWithConfig() 24 | 25 | screen.getByText('data:') 26 | await act(() => sleep(100)) 27 | screen.getByText('data:0') 28 | }) 29 | 30 | // https://codesandbox.io/s/concurrent-swr-case-ii-lr6x4u 31 | it.skip('should do state updates in transitions', async () => { 32 | const key1 = createKey() 33 | const key2 = createKey() 34 | 35 | const log = [] 36 | 37 | function Counter() { 38 | const [count, setCount] = React.useState(0) 39 | 40 | React.useEffect(() => { 41 | const interval = setInterval(() => { 42 | setCount(x => x + 1) 43 | }, 20) 44 | return () => clearInterval(interval) 45 | }, []) 46 | 47 | log.push(count) 48 | 49 | return <>{count} 50 | } 51 | 52 | function Body() { 53 | useSWR(key2, () => createResponse(true, { delay: 1000 }), { 54 | revalidateOnFocus: false, 55 | revalidateOnReconnect: false, 56 | dedupingInterval: 0, 57 | suspense: true 58 | }) 59 | return null 60 | } 61 | 62 | function Page() { 63 | const { data } = useSWR(key1, () => createResponse(true, { delay: 50 }), { 64 | revalidateOnFocus: false, 65 | revalidateOnReconnect: false, 66 | dedupingInterval: 0 67 | }) 68 | 69 | return ( 70 | <> 71 | 72 | {data ? : null} 73 | 74 | ) 75 | } 76 | 77 | await executeWithoutBatching(async () => { 78 | renderWithConfig() 79 | await sleep(500) 80 | }) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # SWR Contribution Guidelines 2 | 3 | Thank you for reading this guide and we appreciate any contribution. 4 | 5 | ## Ask a Question 6 | 7 | You can use the repository's [Discussions](https://github.com/vercel/swr/discussions) page to ask any questions, post feedback, or share your experience on how you use this library. 8 | 9 | ## Report a Bug 10 | 11 | Whenever you find something which is not working properly, please first search the repository's [Issues](https://github.com/vercel/swr/issues) page and make sure it's not reported by someone else already. 12 | 13 | If not, feel free to open an issue with a detailed description of the problem and the expected behavior. And reproduction (for example a [CodeSandbox](https://codesandbox.io) link) will be extremely helpful. 14 | 15 | ## Request for a New Feature 16 | 17 | For new features, it would be great to have some discussions from the community before starting working on it. You can either create an issue (if there isn't one) or post a thread on the [Discussions](https://github.com/vercel/swr/discussions) page to describe the feature that you want to have. 18 | 19 | If possible, you can add another additional context like how this feature can be implemented technically, what other alternative solutions we can have, etc. 20 | 21 | ## Open a PR for Bugfix or Feature 22 | 23 | ### Local Development with Examples 24 | 25 | To run SWR locally, you can start it with any example in the `examples` folder. You need to set up the example and run the command in the root directory for overriding SWR and its dependencies to local assets. 26 | 27 | First of all, build SWR assets 28 | 29 | ```sh 30 | corepack enable 31 | corepack pnpm install 32 | 33 | pnpm watch 34 | ``` 35 | 36 | Install dependency of the target example, for instance `examples/basic`: 37 | 38 | 39 | ```sh 40 | # by default it will run next dev for the example 41 | pnpm next dev examples/basic 42 | ``` 43 | 44 | All examples are built with Next.js, so Next.js commands are all supported: 45 | 46 | ```sh 47 | # if you want to build and start 48 | pnpm next build examples/basic 49 | pnpm next start examples/basic 50 | ``` 51 | ## Update Documentation 52 | 53 | To update the [SWR Documentation](https://swr.vercel.app), you can contribute to the [website repository](https://github.com/vercel/swr-site). 54 | -------------------------------------------------------------------------------- /examples/infinite/pages/index.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import useSWRInfinite from 'swr/infinite' 3 | 4 | import fetch from '../libs/fetch' 5 | 6 | const PAGE_SIZE = 6 7 | 8 | export default function App() { 9 | const [repo, setRepo] = useState('reactjs/react-a11y') 10 | const [val, setVal] = useState(repo) 11 | 12 | const { data, error, mutate, size, setSize, isValidating } = useSWRInfinite( 13 | (index) => 14 | `https://api.github.com/repos/${repo}/issues?per_page=${PAGE_SIZE}&page=${ 15 | index + 1 16 | }`, 17 | fetch 18 | ) 19 | 20 | const issues = data ? [].concat(...data) : [] 21 | const isLoadingInitialData = !data && !error 22 | const isLoadingMore = 23 | isLoadingInitialData || 24 | (size > 0 && data && typeof data[size - 1] === 'undefined') 25 | const isEmpty = data?.[0]?.length === 0 26 | const isReachingEnd = 27 | isEmpty || (data && data[data.length - 1]?.length < PAGE_SIZE) 28 | const isRefreshing = isValidating && data && data.length === size 29 | 30 | return ( 31 |
32 | setVal(e.target.value)} 35 | placeholder="reactjs/react-a11y" 36 | /> 37 | 45 |

46 | showing {size} page(s) of {isLoadingMore ? '...' : issues.length}{' '} 47 | issue(s){' '} 48 | 58 | 61 | 64 |

65 | {isEmpty ?

Yay, no issues found.

: null} 66 | {issues.map((issue) => { 67 | return ( 68 |

69 | - {issue.title} 70 |

71 | ) 72 | })} 73 |
74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /test/use-swr-legacy-react.test.tsx: -------------------------------------------------------------------------------- 1 | // This test case covers special environments such as React <= 17. 2 | 3 | import { screen, render, fireEvent } from '@testing-library/react' 4 | import { act } from 'react' 5 | 6 | // https://github.com/jestjs/jest/issues/11471 7 | jest.mock('react', () => jest.requireActual('react')) 8 | 9 | async function withLegacyReact(runner: () => Promise) { 10 | await jest.isolateModulesAsync(async () => { 11 | await runner() 12 | }) 13 | } 14 | 15 | describe('useSWR - legacy React', () => { 16 | ;(process.env.__SWR_TEST_BUILD ? it.skip : it)( 17 | 'should enable the IS_REACT_LEGACY flag - startTransition', 18 | async () => { 19 | await withLegacyReact(async () => { 20 | // Test mutation and trigger 21 | const useSWRMutation = (await import('swr/mutation')).default 22 | 23 | const waitForNextTick = () => 24 | act(() => new Promise(resolve => setTimeout(resolve, 1))) 25 | const key = Math.random().toString() 26 | 27 | function Page() { 28 | const { data, trigger } = useSWRMutation(key, () => 'data') 29 | return 30 | } 31 | 32 | render() 33 | 34 | // mount 35 | await screen.findByText('pending') 36 | 37 | fireEvent.click(screen.getByText('pending')) 38 | await waitForNextTick() 39 | 40 | screen.getByText('data') 41 | }) 42 | } 43 | ) 44 | 45 | // https://github.com/vercel/swr/blob/cfcfa9e320a59742d41a77e52003127b04378c4f/src/core/use-swr.ts#L345 46 | ;(process.env.__SWR_TEST_BUILD ? it.skip : it)( 47 | 'should enable the IS_REACT_LEGACY flag - unmount check', 48 | async () => { 49 | await withLegacyReact(async () => { 50 | const useSWR = (await import('swr')).default 51 | 52 | const key = Math.random().toString() 53 | 54 | function Page() { 55 | // No fallback data 56 | const { data } = useSWR( 57 | key, 58 | () => 59 | new Promise(resolve => 60 | setTimeout(() => resolve('data'), 100) 61 | ), 62 | { 63 | loadingTimeout: 10 64 | } 65 | ) 66 | return

{data || 'pending'}

67 | } 68 | 69 | render() 70 | 71 | await screen.findByText('data') 72 | }) 73 | } 74 | ) 75 | }) 76 | -------------------------------------------------------------------------------- /test/use-swr-context-config.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen, fireEvent } from '@testing-library/react' 2 | import { act } from 'react' 3 | import useSWR, { 4 | mutate, 5 | SWRConfig, 6 | type SWRConfiguration, 7 | useSWRConfig 8 | } from 'swr' 9 | import { createKey, createResponse, renderWithGlobalCache } from './utils' 10 | import { useCallback, useEffect, useState } from 'react' 11 | 12 | describe('useSWR - context configs', () => { 13 | it('mutate before mount should not block rerender', async () => { 14 | const prefetch = () => createResponse('prefetch-data') 15 | const fetcher = () => createResponse('data') 16 | const key = createKey() 17 | 18 | await act(async () => { 19 | await mutate(key, prefetch) 20 | }) 21 | 22 | function Page() { 23 | const { data } = useSWR(key, fetcher) 24 | return
{data}
25 | } 26 | 27 | renderWithGlobalCache() 28 | // render with the prefetched data 29 | screen.getByText('prefetch-data') 30 | 31 | // render the fetched data 32 | await screen.findByText('data') 33 | }) 34 | }) 35 | 36 | describe('useSWRConfig hook maintains stable reference across re-renders', () => { 37 | it('should maintain the same swrConfig reference when counter updates', () => { 38 | const parentConfig: SWRConfiguration = { 39 | revalidateOnMount: true, 40 | revalidateIfStale: false, 41 | revalidateOnFocus: false, 42 | revalidateOnReconnect: false 43 | } 44 | const counterButtonText = 'counter + 1' 45 | let useSWRConfigReferenceChangedTimes = 0 46 | function Page() { 47 | return ( 48 | 49 | 50 | 51 | ) 52 | } 53 | function ChildComponent() { 54 | const swrConfig = useSWRConfig() 55 | const [, setCounter] = useState(0) 56 | const counterAddOne = useCallback( 57 | () => setCounter(prev => prev + 1), 58 | [setCounter] 59 | ) 60 | useEffect(() => { 61 | useSWRConfigReferenceChangedTimes += 1 62 | }, [swrConfig]) 63 | return 64 | } 65 | render() 66 | fireEvent.click(screen.getByText(counterButtonText)) 67 | fireEvent.click(screen.getByText(counterButtonText)) 68 | fireEvent.click(screen.getByText(counterButtonText)) 69 | expect(useSWRConfigReferenceChangedTimes).toBe(1) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /.github/workflows/test-release.yml: -------------------------------------------------------------------------------- 1 | name: Test and Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - v* 9 | pull_request: 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Install 19 | uses: ./.github/workflows/install 20 | 21 | - name: Lint and test 22 | run: | 23 | pnpm clean 24 | pnpm build 25 | pnpm run-all-checks 26 | npm pack 27 | pnpm attw 28 | pnpm test 29 | pnpm test:build 30 | pnpm test-typing 31 | e2e: 32 | runs-on: ubuntu-latest 33 | container: 34 | image: mcr.microsoft.com/playwright:v1.57.0-noble 35 | steps: 36 | - name: Checkout 37 | uses: actions/checkout@v4 38 | 39 | - name: Install 40 | uses: ./.github/workflows/install 41 | 42 | - name: E2E Tests 43 | run: | 44 | pnpm clean 45 | pnpm build 46 | pnpm build:e2e 47 | pnpm test:e2e 48 | - name: Upload test results 49 | if: always() 50 | uses: actions/upload-artifact@v4 51 | with: 52 | name: playwright-report 53 | path: playwright-report 54 | release: 55 | runs-on: ubuntu-latest 56 | needs: ["test", "e2e"] 57 | if: startsWith(github.ref, 'refs/tags/v') 58 | permissions: 59 | id-token: write 60 | steps: 61 | - name: Check out 62 | uses: actions/checkout@v4 63 | 64 | - name: Install 65 | uses: ./.github/workflows/install 66 | 67 | - name: Determine tag 68 | id: determine_tag 69 | run: | 70 | echo "tag=$(echo $GITHUB_REF | grep -Eo 'alpha|beta|canary|rc')" >> $GITHUB_OUTPUT 71 | 72 | - name: Publish to versioned tag 73 | if: steps.determine_tag.outputs.tag != '' 74 | run: | 75 | echo "Publishing to ${{ steps.determine_tag.outputs.tag }} tag" 76 | npm publish --access public --no-git-checks --provenance --tag ${{ steps.determine_tag.outputs.tag }} 77 | env: 78 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN_ELEVATED }} 79 | 80 | - name: Publish to latest 81 | if: steps.determine_tag.outputs.tag == '' 82 | run: | 83 | echo "Publishing to latest" 84 | npm publish --access public --no-git-checks --provenance 85 | env: 86 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN_ELEVATED }} 87 | -------------------------------------------------------------------------------- /src/_internal/utils/config-context.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import type { FC, PropsWithChildren } from 'react' 4 | import { 5 | createContext, 6 | createElement, 7 | useContext, 8 | useMemo, 9 | useRef 10 | } from 'react' 11 | import { cache as defaultCache } from './config' 12 | import { initCache } from './cache' 13 | import { mergeConfigs } from './merge-config' 14 | import { UNDEFINED, mergeObjects, isFunction } from './shared' 15 | import { useIsomorphicLayoutEffect } from './env' 16 | import type { SWRConfiguration, FullConfiguration } from '../types' 17 | 18 | export const SWRConfigContext = createContext>({}) 19 | 20 | const SWRConfig: FC< 21 | PropsWithChildren<{ 22 | value?: 23 | | SWRConfiguration 24 | | ((parentConfig?: SWRConfiguration) => SWRConfiguration) 25 | }> 26 | > = props => { 27 | const { value } = props 28 | const parentConfig = useContext(SWRConfigContext) 29 | const isFunctionalConfig = isFunction(value) 30 | const config = useMemo( 31 | () => (isFunctionalConfig ? value(parentConfig) : value), 32 | [isFunctionalConfig, parentConfig, value] 33 | ) 34 | // Extend parent context values and middleware. 35 | const extendedConfig = useMemo( 36 | () => (isFunctionalConfig ? config : mergeConfigs(parentConfig, config)), 37 | [isFunctionalConfig, parentConfig, config] 38 | ) 39 | 40 | // Should not use the inherited provider. 41 | const provider = config && config.provider 42 | 43 | // initialize the cache only on first access. 44 | const cacheContextRef = useRef>(UNDEFINED) 45 | if (provider && !cacheContextRef.current) { 46 | cacheContextRef.current = initCache( 47 | provider((extendedConfig as any).cache || defaultCache), 48 | config 49 | ) 50 | } 51 | const cacheContext = cacheContextRef.current 52 | 53 | // Override the cache if a new provider is given. 54 | if (cacheContext) { 55 | ;(extendedConfig as any).cache = cacheContext[0] 56 | ;(extendedConfig as any).mutate = cacheContext[1] 57 | } 58 | 59 | // Unsubscribe events. 60 | useIsomorphicLayoutEffect(() => { 61 | if (cacheContext) { 62 | // eslint-disable-next-line @typescript-eslint/no-unused-expressions 63 | cacheContext[2] && cacheContext[2]() 64 | return cacheContext[3] 65 | } 66 | }, []) 67 | 68 | return createElement( 69 | SWRConfigContext.Provider, 70 | mergeObjects(props, { 71 | value: extendedConfig 72 | }) 73 | ) 74 | } 75 | 76 | export default SWRConfig 77 | -------------------------------------------------------------------------------- /src/_internal/utils/hash.ts: -------------------------------------------------------------------------------- 1 | import { OBJECT, isUndefined } from './shared' 2 | 3 | // use WeakMap to store the object->key mapping 4 | // so the objects can be garbage collected. 5 | // WeakMap uses a hashtable under the hood, so the lookup 6 | // complexity is almost O(1). 7 | const table = new WeakMap() 8 | 9 | const getTypeName = (value: any) => OBJECT.prototype.toString.call(value) 10 | 11 | const isObjectTypeName = (typeName: string, type: string) => 12 | typeName === `[object ${type}]` 13 | 14 | // counter of the key 15 | let counter = 0 16 | 17 | // A stable hash implementation that supports: 18 | // - Fast and ensures unique hash properties 19 | // - Handles unserializable values 20 | // - Handles object key ordering 21 | // - Generates short results 22 | // 23 | // This is not a serialization function, and the result is not guaranteed to be 24 | // parsable. 25 | export const stableHash = (arg: any): string => { 26 | const type = typeof arg 27 | const typeName = getTypeName(arg) 28 | const isDate = isObjectTypeName(typeName, 'Date') 29 | const isRegex = isObjectTypeName(typeName, 'RegExp') 30 | const isPlainObject = isObjectTypeName(typeName, 'Object') 31 | let result: any 32 | let index: any 33 | 34 | if (OBJECT(arg) === arg && !isDate && !isRegex) { 35 | // Object/function, not null/date/regexp. Use WeakMap to store the id first. 36 | // If it's already hashed, directly return the result. 37 | result = table.get(arg) 38 | if (result) return result 39 | 40 | // Store the hash first for circular reference detection before entering the 41 | // recursive `stableHash` calls. 42 | // For other objects like set and map, we use this id directly as the hash. 43 | result = ++counter + '~' 44 | table.set(arg, result) 45 | 46 | if (Array.isArray(arg)) { 47 | // Array. 48 | result = '@' 49 | for (index = 0; index < arg.length; index++) { 50 | result += stableHash(arg[index]) + ',' 51 | } 52 | table.set(arg, result) 53 | } 54 | if (isPlainObject) { 55 | // Object, sort keys. 56 | result = '#' 57 | const keys = OBJECT.keys(arg).sort() 58 | while (!isUndefined((index = keys.pop() as string))) { 59 | if (!isUndefined(arg[index])) { 60 | result += index + ':' + stableHash(arg[index]) + ',' 61 | } 62 | } 63 | table.set(arg, result) 64 | } 65 | } else { 66 | result = isDate 67 | ? arg.toJSON() 68 | : type == 'symbol' 69 | ? arg.toString() 70 | : type == 'string' 71 | ? JSON.stringify(arg) 72 | : '' + arg 73 | } 74 | 75 | return result 76 | } 77 | -------------------------------------------------------------------------------- /examples/infinite-scroll/pages/index.js: -------------------------------------------------------------------------------- 1 | import useSWRInfinite from 'swr/infinite' 2 | import { useState, useRef, useEffect } from 'react' 3 | 4 | import fetcher from '../libs/fetch' 5 | import useOnScreen from '../hooks/useOnScreen' 6 | 7 | const PAGE_SIZE = 6 8 | 9 | const getKey = (pageIndex, previousPageData, repo, pageSize) => { 10 | if (previousPageData && !previousPageData.length) return null // reached the end 11 | 12 | return `https://api.github.com/repos/${repo}/issues?per_page=${pageSize}&page=${ 13 | pageIndex + 1 14 | }` 15 | } 16 | 17 | export default function App() { 18 | const ref = useRef() 19 | const [repo, setRepo] = useState('facebook/react') 20 | const [val, setVal] = useState(repo) 21 | 22 | const isVisible = useOnScreen(ref) 23 | 24 | const { data, error, mutate, size, setSize, isValidating } = useSWRInfinite( 25 | (...args) => getKey(...args, repo, PAGE_SIZE), 26 | fetcher 27 | ) 28 | 29 | const issues = data ? [].concat(...data) : [] 30 | const isLoadingInitialData = !data && !error 31 | const isLoadingMore = 32 | isLoadingInitialData || 33 | (size > 0 && data && typeof data[size - 1] === 'undefined') 34 | const isEmpty = data?.[0]?.length === 0 35 | const isReachingEnd = size === PAGE_SIZE 36 | const isRefreshing = isValidating && data && data.length === size 37 | 38 | useEffect(() => { 39 | if (isVisible && !isReachingEnd && !isRefreshing) { 40 | setSize(size + 1) 41 | } 42 | }, [isVisible, isRefreshing]) 43 | 44 | return ( 45 |
46 | setVal(e.target.value)} 49 | placeholder="facebook/react" 50 | /> 51 | 59 |

60 | showing {size} page(s) of {isLoadingMore ? '...' : issues.length}{' '} 61 | issue(s){' '} 62 | 65 | 68 |

69 | {isEmpty ?

Yay, no issues found.

: null} 70 | {issues.map((issue) => { 71 | return ( 72 |

73 | - {issue.title} 74 |

75 | ) 76 | })} 77 |
78 | {isLoadingMore ? 'loading...' : isReachingEnd ? 'no more issues' : ''} 79 |
80 |
81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /test/use-swr-streaming-ssr.test.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense, act } from 'react' 2 | import useSWR from 'swr' 3 | import { 4 | createKey, 5 | createResponse, 6 | renderWithConfig, 7 | hydrateWithConfig, 8 | mockConsoleForHydrationErrors, 9 | sleep 10 | } from './utils' 11 | 12 | describe('useSWR - streaming', () => { 13 | afterEach(() => { 14 | jest.clearAllMocks() 15 | jest.restoreAllMocks() 16 | }) 17 | 18 | it('should match ssr result when hydrating', async () => { 19 | const ensureAndUnmock = mockConsoleForHydrationErrors() 20 | 21 | const key = createKey() 22 | 23 | // A block fetches the data and updates the cache. 24 | function Block() { 25 | const { data } = useSWR(key, () => createResponse('SWR', { delay: 10 })) 26 | return
{data || 'undefined'}
27 | } 28 | 29 | const container = document.createElement('div') 30 | container.innerHTML = '
undefined
' 31 | await hydrateWithConfig(, container) 32 | ensureAndUnmock() 33 | }) 34 | 35 | // NOTE: this test is failing because it's not possible to test this behavior 36 | // in JSDOM. We need to test this in a real browser. 37 | it.failing( 38 | 'should match the ssr result when streaming and partially hydrating', 39 | async () => { 40 | const key = createKey() 41 | 42 | const dataDuringHydration = {} 43 | 44 | // A block fetches the data and updates the cache. 45 | function Block({ suspense, delay, id }) { 46 | const { data } = useSWR(key, () => createResponse('SWR', { delay }), { 47 | suspense 48 | }) 49 | 50 | // The first render is always hydration in our case. 51 | if (!dataDuringHydration[id]) { 52 | dataDuringHydration[id] = data || 'undefined' 53 | } 54 | 55 | return
{data || 'undefined'}
56 | } 57 | 58 | // In this example, a will be hydrated first and b will still be streamed. 59 | // When a is hydrated, it will update the client cache to SWR, and when 60 | // b is being hydrated, it should NOT read that cache. 61 | renderWithConfig( 62 | <> 63 | 64 | 65 | 66 | 67 | 68 | ) 69 | 70 | // The SSR result will always be 2 undefined values because data fetching won't 71 | // happen on the server: 72 | //
undefined
73 | //
undefined
74 | 75 | // Wait for streaming to finish. 76 | await act(() => sleep(50)) 77 | 78 | expect(dataDuringHydration).toEqual({ 79 | a: 'undefined', 80 | b: 'undefined' 81 | }) 82 | } 83 | ) 84 | }) 85 | -------------------------------------------------------------------------------- /src/mutation/state.ts: -------------------------------------------------------------------------------- 1 | import type { MutableRefObject, TransitionFunction } from 'react' 2 | import React, { useRef, useCallback, useState } from 'react' 3 | import { useIsomorphicLayoutEffect, IS_REACT_LEGACY } from '../_internal' 4 | 5 | export const startTransition: (scope: TransitionFunction) => void = 6 | IS_REACT_LEGACY 7 | ? cb => { 8 | cb() 9 | } 10 | : React.startTransition 11 | 12 | /** 13 | * An implementation of state with dependency-tracking. 14 | * @param initialState - The initial state object. 15 | */ 16 | export const useStateWithDeps = >( 17 | initialState: S 18 | ): [ 19 | MutableRefObject, 20 | Record, 21 | (payload: Partial) => void 22 | ] => { 23 | const [, rerender] = useState>({}) 24 | const unmountedRef = useRef(false) 25 | const stateRef = useRef(initialState) 26 | 27 | // If a state property (data, error, or isValidating) is accessed by the render 28 | // function, we mark the property as a dependency so if it is updated again 29 | // in the future, we trigger a rerender. 30 | // This is also known as dependency-tracking. 31 | const stateDependenciesRef = useRef>({ 32 | data: false, 33 | error: false, 34 | isValidating: false 35 | } as Record) 36 | 37 | /** 38 | * Updates state and triggers re-render if necessary. 39 | * @param payload To change stateRef, pass the values explicitly to setState: 40 | * @example 41 | * ```js 42 | * setState({ 43 | * isValidating: false 44 | * data: newData // set data to newData 45 | * error: undefined // set error to undefined 46 | * }) 47 | * 48 | * setState({ 49 | * isValidating: false 50 | * data: undefined // set data to undefined 51 | * error: err // set error to err 52 | * }) 53 | * ``` 54 | */ 55 | const setState = useCallback((payload: Partial) => { 56 | let shouldRerender = false 57 | 58 | const currentState = stateRef.current 59 | for (const key in payload) { 60 | if (Object.prototype.hasOwnProperty.call(payload, key)) { 61 | const k = key as keyof S 62 | 63 | // If the property has changed, update the state and mark rerender as 64 | // needed. 65 | if (currentState[k] !== payload[k]) { 66 | currentState[k] = payload[k]! 67 | 68 | // If the property is accessed by the component, a rerender should be 69 | // triggered. 70 | if (stateDependenciesRef.current[k]) { 71 | shouldRerender = true 72 | } 73 | } 74 | } 75 | } 76 | 77 | if (shouldRerender && !unmountedRef.current) { 78 | rerender({}) 79 | } 80 | }, []) 81 | 82 | useIsomorphicLayoutEffect(() => { 83 | unmountedRef.current = false 84 | return () => { 85 | unmountedRef.current = true 86 | } 87 | }) 88 | 89 | return [stateRef, stateDependenciesRef.current, setState] 90 | } 91 | -------------------------------------------------------------------------------- /test/type/subscription.ts: -------------------------------------------------------------------------------- 1 | import useSWRSubscription from 'swr/subscription' 2 | import type { SWRSubscriptionOptions, SWRSubscription } from 'swr/subscription' 3 | import { expectType, truthy } from './utils' 4 | 5 | export function useTestSubscription() { 6 | useSWRSubscription( 7 | 'key', 8 | (key, { next: _ }: SWRSubscriptionOptions) => { 9 | expectType<'key'>(key) 10 | return () => {} 11 | } 12 | ) 13 | useSWRSubscription( 14 | truthy() ? 'key' : undefined, 15 | (key, { next: _ }: SWRSubscriptionOptions) => { 16 | expectType<'key'>(key) 17 | return () => {} 18 | } 19 | ) 20 | useSWRSubscription( 21 | ['key', 1], 22 | (key, { next: _ }: SWRSubscriptionOptions) => { 23 | expectType<[string, number]>(key) 24 | return () => {} 25 | } 26 | ) 27 | useSWRSubscription( 28 | truthy() ? ['key', 1] : undefined, 29 | (key, { next: _ }: SWRSubscriptionOptions) => { 30 | expectType<[string, number]>(key) 31 | return () => {} 32 | } 33 | ) 34 | useSWRSubscription( 35 | { foo: 'bar' }, 36 | (key, { next: _ }: SWRSubscriptionOptions) => { 37 | expectType<{ foo: string }>(key) 38 | return () => {} 39 | } 40 | ) 41 | useSWRSubscription( 42 | truthy() ? { foo: 'bar' } : undefined, 43 | (key, { next: _ }: SWRSubscriptionOptions) => { 44 | expectType<{ foo: string }>(key) 45 | return () => {} 46 | } 47 | ) 48 | 49 | useSWRSubscription( 50 | () => 'key', 51 | (key, { next: _ }: SWRSubscriptionOptions) => { 52 | expectType(key) 53 | return () => {} 54 | } 55 | ) 56 | useSWRSubscription( 57 | () => (truthy() ? 'key' : undefined), 58 | (key, { next: _ }: SWRSubscriptionOptions) => { 59 | expectType<'key'>(key) 60 | return () => {} 61 | } 62 | ) 63 | useSWRSubscription( 64 | () => ['key', 1], 65 | (key, { next: _ }: SWRSubscriptionOptions) => { 66 | expectType<[string, number]>(key) 67 | return () => {} 68 | } 69 | ) 70 | useSWRSubscription( 71 | () => (truthy() ? ['key', 1] : undefined), 72 | (key, { next: _ }: SWRSubscriptionOptions) => { 73 | expectType<[string, number]>(key) 74 | return () => {} 75 | } 76 | ) 77 | useSWRSubscription( 78 | () => ({ foo: 'bar' }), 79 | (key, { next: _ }: SWRSubscriptionOptions) => { 80 | expectType<{ foo: string }>(key) 81 | return () => {} 82 | } 83 | ) 84 | useSWRSubscription( 85 | () => (truthy() ? { foo: 'bar' } : undefined), 86 | (key, { next: _ }: SWRSubscriptionOptions) => { 87 | expectType<{ foo: string }>(key) 88 | return () => {} 89 | } 90 | ) 91 | 92 | const sub: SWRSubscription = (_, { next: __ }) => { 93 | return () => {} 94 | } 95 | const { data: data2, error: error2 } = useSWRSubscription('key', sub) 96 | expectType(data2) 97 | expectType(error2) 98 | } 99 | --------------------------------------------------------------------------------