├── .codesandbox
└── ci.json
├── .editorconfig
├── .eslintrc
├── .github
├── CODEOWNERS
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── config.yml
├── SECURITY.md
└── workflows
│ ├── install
│ └── action.yml
│ ├── test-canary.yml
│ ├── test-legacy-react.yml
│ ├── test-release.yml
│ └── trigger-release.yml
├── .gitignore
├── .husky
└── pre-commit
├── .npmrc
├── .swcrc
├── LICENSE
├── README.md
├── _internal
└── package.json
├── e2e
├── site
│ ├── README.md
│ ├── app
│ │ ├── basic-ssr
│ │ │ ├── block.tsx
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ ├── mutate-server-action
│ │ │ ├── action.tsx
│ │ │ └── page.tsx
│ │ ├── page.tsx
│ │ ├── partially-hydrate
│ │ │ ├── layout.tsx
│ │ │ ├── loading.tsx
│ │ │ ├── page.tsx
│ │ │ └── use-data.tsx
│ │ ├── react-server-entry
│ │ │ └── page.tsx
│ │ ├── suspense-after-preload
│ │ │ ├── page.tsx
│ │ │ └── remote-data.tsx
│ │ ├── suspense-fallback
│ │ │ ├── layout.tsx
│ │ │ └── promise
│ │ │ │ └── page.tsx
│ │ └── suspense-retry-18-3
│ │ │ ├── manual-retry.tsx
│ │ │ ├── page.tsx
│ │ │ └── use-remote-data.ts
│ ├── component
│ │ ├── manual-retry-mutate.tsx
│ │ ├── manual-retry.tsx
│ │ └── use-remote-data.ts
│ ├── lib
│ │ └── use-debug-history.ts
│ ├── next-env.d.ts
│ ├── next.config.js
│ ├── package.json
│ ├── pages
│ │ ├── api
│ │ │ ├── data.ts
│ │ │ └── retry.ts
│ │ ├── suspense-retry-18-2.tsx
│ │ └── suspense-retry-mutate.tsx
│ ├── public
│ │ └── favicon.ico
│ └── tsconfig.json
└── test
│ ├── initial-render.test.ts
│ ├── mutate-server-action.test.ts
│ ├── stream-ssr.test.ts
│ ├── suspense-fallback.test.ts
│ └── tsconfig.json
├── examples
├── .eslintrc
├── api-hooks
│ ├── README.md
│ ├── hooks
│ │ ├── use-projects.js
│ │ └── use-repository.js
│ ├── libs
│ │ └── fetch.js
│ ├── package.json
│ └── pages
│ │ ├── [user]
│ │ └── [repo].js
│ │ ├── api
│ │ └── data.js
│ │ └── index.js
├── autocomplete-suggestions
│ ├── README.md
│ ├── libs
│ │ └── fetcher.js
│ ├── package.json
│ └── pages
│ │ ├── api
│ │ └── suggestions.js
│ │ └── index.js
├── axios-typescript
│ ├── README.md
│ ├── libs
│ │ └── useRequest.ts
│ ├── next-env.d.ts
│ ├── package.json
│ ├── pages
│ │ ├── [user]
│ │ │ └── [repo].tsx
│ │ ├── api
│ │ │ └── data.ts
│ │ └── index.tsx
│ └── tsconfig.json
├── axios
│ ├── README.md
│ ├── libs
│ │ └── useRequest.js
│ ├── package.json
│ └── pages
│ │ ├── [user]
│ │ └── [repo].js
│ │ ├── api
│ │ └── data.js
│ │ └── index.js
├── basic-typescript
│ ├── README.md
│ ├── libs
│ │ └── fetch.ts
│ ├── next-env.d.ts
│ ├── package.json
│ ├── pages
│ │ ├── [user]
│ │ │ └── [repo].tsx
│ │ ├── api
│ │ │ └── data.ts
│ │ └── index.tsx
│ └── tsconfig.json
├── basic
│ ├── README.md
│ ├── libs
│ │ └── fetch.js
│ ├── package.json
│ └── pages
│ │ ├── [user]
│ │ └── [repo].js
│ │ ├── api
│ │ └── data.js
│ │ └── index.js
├── focus-revalidate
│ ├── README.md
│ ├── components
│ │ └── button.js
│ ├── libs
│ │ ├── auth.js
│ │ └── fetch.js
│ ├── package.json
│ └── pages
│ │ ├── api
│ │ └── user.js
│ │ └── index.js
├── global-fetcher
│ ├── README.md
│ ├── libs
│ │ └── fetch.js
│ ├── package.json
│ └── pages
│ │ ├── [user]
│ │ └── [repo].js
│ │ ├── _app.js
│ │ ├── api
│ │ └── data.js
│ │ └── index.js
├── infinite-scroll
│ ├── README.md
│ ├── hooks
│ │ └── useOnScreen.js
│ ├── libs
│ │ └── fetch.js
│ ├── package.json
│ └── pages
│ │ └── index.js
├── infinite
│ ├── README.md
│ ├── libs
│ │ └── fetch.js
│ ├── package.json
│ └── pages
│ │ └── index.js
├── local-state-sharing
│ ├── README.md
│ ├── libs
│ │ └── store.js
│ ├── package.json
│ └── pages
│ │ └── index.js
├── optimistic-ui-immer
│ ├── README.md
│ ├── libs
│ │ └── fetch.js
│ ├── package.json
│ └── pages
│ │ ├── api
│ │ └── data.js
│ │ └── index.js
├── optimistic-ui
│ ├── README.md
│ ├── libs
│ │ └── fetch.js
│ ├── package.json
│ ├── pages
│ │ ├── _app.js
│ │ ├── api
│ │ │ └── todos.js
│ │ └── index.js
│ └── styles.css
├── prefetch-preload
│ ├── README.md
│ ├── libs
│ │ └── fetch.js
│ ├── package.json
│ └── pages
│ │ ├── [user]
│ │ └── [repo].js
│ │ ├── api
│ │ └── data.js
│ │ └── index.js
├── refetch-interval
│ ├── README.md
│ ├── components
│ │ └── button.js
│ ├── libs
│ │ └── fetch.js
│ ├── package.json
│ └── pages
│ │ ├── api
│ │ └── data.js
│ │ └── index.js
├── server-render
│ ├── README.md
│ ├── libs
│ │ └── fetcher.js
│ ├── package.json
│ └── pages
│ │ ├── [pokemon].js
│ │ └── index.js
├── storage-tab-sync
│ ├── README.md
│ ├── libs
│ │ └── storage.js
│ ├── package.json
│ └── pages
│ │ └── index.js
├── subscription
│ ├── README.md
│ ├── package.json
│ └── pages
│ │ └── index.js
├── suspense-retry
│ ├── app
│ │ ├── api
│ │ │ └── route.ts
│ │ ├── favicon.ico
│ │ ├── layout.tsx
│ │ ├── manual-retry.tsx
│ │ ├── page.tsx
│ │ └── use-remote-data.ts
│ ├── next-env.d.ts
│ ├── next.config.js
│ ├── package.json
│ ├── pages
│ │ └── retry.tsx
│ └── tsconfig.json
└── suspense
│ ├── README.md
│ ├── app
│ ├── favicon.ico
│ ├── layout.jsx
│ └── rsc
│ │ ├── [user]
│ │ └── [repo]
│ │ │ ├── error.jsx
│ │ │ ├── loading.jsx
│ │ │ ├── page.jsx
│ │ │ └── repo.jsx
│ │ ├── loading.jsx
│ │ ├── page.jsx
│ │ └── repos.jsx
│ ├── components
│ └── error-handling.js
│ ├── libs
│ └── fetch.js
│ ├── next-env.d.ts
│ ├── package.json
│ └── pages
│ ├── [user]
│ └── [repo].js
│ ├── api
│ └── data.js
│ └── index.js
├── immutable
└── package.json
├── infinite
└── package.json
├── jest.config.build.js
├── jest.config.js
├── mutation
└── package.json
├── package.json
├── playwright.config.js
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── scripts
└── bump-next-version.js
├── src
├── _internal
│ ├── constants.ts
│ ├── events.ts
│ ├── index.react-server.ts
│ ├── index.ts
│ ├── types.ts
│ └── utils
│ │ ├── cache.ts
│ │ ├── config-context.ts
│ │ ├── config.ts
│ │ ├── devtools.ts
│ │ ├── env.ts
│ │ ├── global-state.ts
│ │ ├── hash.ts
│ │ ├── helper.ts
│ │ ├── merge-config.ts
│ │ ├── middleware-preset.ts
│ │ ├── mutate.ts
│ │ ├── normalize-args.ts
│ │ ├── preload.ts
│ │ ├── resolve-args.ts
│ │ ├── serialize.ts
│ │ ├── shared.ts
│ │ ├── subscribe-key.ts
│ │ ├── timestamp.ts
│ │ ├── use-swr-config.ts
│ │ ├── web-preset.ts
│ │ └── with-middleware.ts
├── immutable
│ └── index.ts
├── index
│ ├── config.ts
│ ├── index.react-server.ts
│ ├── index.ts
│ ├── serialize.ts
│ └── use-swr.ts
├── infinite
│ ├── index.react-server.ts
│ ├── index.ts
│ ├── serialize.ts
│ └── types.ts
├── mutation
│ ├── index.ts
│ ├── state.ts
│ └── types.ts
└── subscription
│ ├── index.ts
│ └── types.ts
├── subscription
└── package.json
├── test
├── jest-setup.ts
├── tsconfig.json
├── type
│ ├── .eslintrc
│ ├── config.tsx
│ ├── fetcher.ts
│ ├── helper-types.tsx
│ ├── internal.tsx
│ ├── mutate.ts
│ ├── mutation.ts
│ ├── option-fetcher.ts
│ ├── preload.ts
│ ├── subscription.ts
│ ├── trigger.ts
│ ├── tsconfig.json
│ └── utils.ts
├── unit
│ ├── serialize.test.ts
│ ├── utils.test.tsx
│ └── web-preset.test.ts
├── use-swr-cache.test.tsx
├── use-swr-concurrent-rendering.test.tsx
├── use-swr-config-callbacks.test.tsx
├── use-swr-config.test.tsx
├── use-swr-context-config.test.tsx
├── use-swr-devtools.test.tsx
├── use-swr-error.test.tsx
├── use-swr-fetcher.test.tsx
├── use-swr-focus.test.tsx
├── use-swr-immutable.test.tsx
├── use-swr-infinite-preload.test.tsx
├── use-swr-infinite.test.tsx
├── use-swr-integration.test.tsx
├── use-swr-key.test.tsx
├── use-swr-laggy.test.tsx
├── use-swr-legacy-react.test.tsx
├── use-swr-loading.test.tsx
├── use-swr-local-mutation.test.tsx
├── use-swr-middlewares.test.tsx
├── use-swr-node-env.test.tsx
├── use-swr-offline.test.tsx
├── use-swr-preload.test.tsx
├── use-swr-promise.test.tsx
├── use-swr-reconnect.test.tsx
├── use-swr-refresh.test.tsx
├── use-swr-remote-mutation.test.tsx
├── use-swr-revalidate.test.tsx
├── use-swr-server.test.tsx
├── use-swr-streaming-ssr.test.tsx
├── use-swr-subscription.test.tsx
├── use-swr-suspense.test.tsx
└── utils.tsx
└── tsconfig.json
/.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 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*.{js,ts,jsx,tsx}]
4 | indent_size = 2
5 | indent_style = space
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "parserOptions": {
4 | "ecmaVersion": 8,
5 | "sourceType": "module",
6 | "ecmaFeatures": {
7 | "impliedStrict": true,
8 | "experimentalObjectRestSpread": true
9 | },
10 | "allowImportExportEverywhere": true,
11 | "project": ["**/tsconfig.json"]
12 | },
13 | "plugins": ["@typescript-eslint", "react-hooks"],
14 | "extends": [
15 | "eslint:recommended",
16 | "plugin:react/recommended",
17 | "plugin:@typescript-eslint/recommended",
18 | "prettier",
19 | "plugin:jest-dom/recommended",
20 | "plugin:testing-library/react",
21 | "plugin:react/jsx-runtime"
22 | ],
23 | "settings": {
24 | "react": {
25 | "version": "detect"
26 | }
27 | },
28 | "env": {
29 | "es6": true,
30 | "browser": true,
31 | "node": true,
32 | "jest": true
33 | },
34 | "rules": {
35 | "func-names": [2, "as-needed"],
36 | "no-shadow": 0,
37 | "@typescript-eslint/no-shadow": 2,
38 | "@typescript-eslint/explicit-function-return-type": 0,
39 | "@typescript-eslint/no-unused-vars": [0, {"argsIgnorePattern": "^_"}],
40 | "@typescript-eslint/no-use-before-define": 0,
41 | "@typescript-eslint/ban-ts-ignore": 0,
42 | "@typescript-eslint/no-empty-function": 0,
43 | "@typescript-eslint/ban-ts-comment": 0,
44 | "@typescript-eslint/no-var-requires": 0,
45 | "@typescript-eslint/no-explicit-any": 0,
46 | "@typescript-eslint/explicit-module-boundary-types": 0,
47 | "@typescript-eslint/consistent-type-imports": [2, {"prefer": "type-imports"}],
48 | "@typescript-eslint/ban-types": 0,
49 | "react-hooks/rules-of-hooks": 2,
50 | "react-hooks/exhaustive-deps": 1,
51 | "react/prop-types": 0,
52 | "testing-library/no-unnecessary-act": 0
53 | },
54 | "ignorePatterns": ["dist/", "node_modules", "scripts", "examples"]
55 | }
56 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @shuding @huozhi
2 |
--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to make participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at coc@vercel.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality concerning the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 18
14 | uses: actions/setup-node@v4
15 | with:
16 | node-version: 20
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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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.34.3-focal
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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | pnpm lint-staged && pnpm types:check
5 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | # prevent sub-packages from installing peer-deps (multiple react versions)
2 | auto-install-peers=false
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/_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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/e2e/site/app/basic-ssr/page.tsx:
--------------------------------------------------------------------------------
1 | import Block from './block'
2 |
3 | export default function BasicSSRPage() {
4 | return
5 | }
6 |
--------------------------------------------------------------------------------
/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 |
{children}
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
trigger()}>mutate
13 |
isMutating: {isMutating.toString()}
14 |
data: {data?.result}
15 |
16 | )
17 | }
18 |
19 | export default Page
20 |
--------------------------------------------------------------------------------
/e2e/site/app/page.tsx:
--------------------------------------------------------------------------------
1 | export default function Home() {
2 | return SWR E2E Test
3 | }
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/e2e/site/app/partially-hydrate/loading.tsx:
--------------------------------------------------------------------------------
1 | export default function Loading() {
2 | return Loading...
3 | }
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 | {
29 | preload(key, fetcher)
30 | toggle(!show)
31 | }}
32 | >
33 | preload
34 |
35 | {show ? (
36 | loading
}>
37 |
38 |
39 | ) : null}
40 |
41 | )
42 | }
43 |
44 | export default Comp
45 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/e2e/site/app/suspense-retry-18-3/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 |
{
17 | resetErrorBoundary()
18 | }}
19 | >
20 | retry
21 |
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 |
--------------------------------------------------------------------------------
/e2e/site/app/suspense-retry-18-3/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 |
--------------------------------------------------------------------------------
/e2e/site/app/suspense-retry-18-3/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 |
--------------------------------------------------------------------------------
/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 |
{
32 | await mutate(key, fetcher)
33 | resetErrorBoundary()
34 | }}
35 | >
36 | retry
37 |
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 |
--------------------------------------------------------------------------------
/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 |
{
16 | resetErrorBoundary()
17 | }}
18 | >
19 | retry
20 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/e2e/site/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
5 | // NOTE: This file should not be edited
6 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
7 |
--------------------------------------------------------------------------------
/e2e/site/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {}
3 |
4 | module.exports = nextConfig
5 |
--------------------------------------------------------------------------------
/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": "^20.2.5",
13 | "@types/react": "^18.2.8",
14 | "@types/react-dom": "18.2.4",
15 | "next": "^15.0.4",
16 | "react": "^18.3.1",
17 | "react-dom": "^18.3.1",
18 | "typescript": "5.1.3",
19 | "swr": "*"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/e2e/site/pages/suspense-retry-18-2.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/e2e/site/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 |
--------------------------------------------------------------------------------
/e2e/test/initial-render.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable testing-library/prefer-screen-queries */
2 | import { test, expect } from '@playwright/test'
3 |
4 | test.describe('rendering', () => {
5 | test('suspense with preload', async ({ page }) => {
6 | await page.goto('./suspense-after-preload', { waitUntil: 'commit' })
7 | await page.getByRole('button', { name: 'preload' }).click()
8 | await expect(page.getByText('suspense-after-preload')).toBeVisible()
9 | })
10 | test('should be able to retry in suspense with react 18.3', async ({
11 | page
12 | }) => {
13 | await page.goto('./suspense-retry-18-3', { 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 | test('should be able to retry in suspense with react 18.2', async ({
19 | page
20 | }) => {
21 | await page.goto('./suspense-retry-18-2', { waitUntil: 'commit' })
22 | await expect(page.getByText('Something went wrong')).toBeVisible()
23 | await page.getByRole('button', { name: 'retry' }).click()
24 | await expect(page.getByText('data: SWR suspense retry works')).toBeVisible()
25 | })
26 | test('should be able to retry in suspense with mutate', async ({ page }) => {
27 | await page.goto('./suspense-retry-mutate', { waitUntil: 'commit' })
28 | await expect(page.getByText('Something went wrong')).toBeVisible()
29 | await page.getByRole('button', { name: 'retry' }).click()
30 | await expect(page.getByText('data: SWR suspense retry works')).toBeVisible()
31 | })
32 | test('should be able to use `unstable_serialize` in server component', async ({
33 | page
34 | }) => {
35 | await page.goto('./react-server-entry', { waitUntil: 'commit' })
36 | await expect(page.getByText('unstable_serialize: useSWR')).toBeVisible()
37 | await expect(
38 | page.getByText('infinite_unstable_serialize: $inf$useSWRInfinite')
39 | ).toBeVisible()
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/e2e/test/mutate-server-action.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable testing-library/prefer-screen-queries */
2 | import { test, expect } from '@playwright/test'
3 |
4 | test('mutate-server-action', async ({ page }) => {
5 | await page.goto('./mutate-server-action')
6 | await page.getByRole('button', { name: 'mutate' }).click()
7 | await expect(page.getByText('isMutating: true')).toBeVisible()
8 | await expect(page.getByText('data: ')).toBeVisible()
9 | await page.waitForTimeout(500)
10 | await expect(page.getByText('isMutating: false')).toBeVisible()
11 | await expect(page.getByText('data: 10086')).toBeVisible()
12 | })
13 |
--------------------------------------------------------------------------------
/e2e/test/stream-ssr.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable testing-library/prefer-screen-queries */
2 | import { test, expect } from '@playwright/test'
3 |
4 | test.describe('Stream SSR', () => {
5 | test('Basic SSR', async ({ page }) => {
6 | const log: any[] = []
7 | await page.exposeFunction('consoleError', (msg: any) => log.push(msg))
8 | await page.addInitScript(`
9 | const onError = window.onerror
10 | window.onerror = (...args) => {
11 | consoleError(...args)
12 | onError(...args)
13 | }
14 | `)
15 | await page.goto('./basic-ssr', { waitUntil: 'commit' })
16 | await expect(page.getByText('result:undefined')).toBeVisible()
17 | await expect(page.getByText('result:SSR Works')).toBeVisible()
18 | await expect(page.getByText('history:[null,"SSR Works"]')).toBeVisible()
19 | expect(log).toHaveLength(0)
20 | })
21 |
22 | test('Partially Hydrate', async ({ page }) => {
23 | const log: any[] = []
24 | await page.exposeFunction('consoleError', (msg: any) => log.push(msg))
25 | await page.addInitScript(`
26 | const onError = window.onerror
27 | window.onerror = (...args) => {
28 | consoleError(...args)
29 | onError(...args)
30 | }
31 | `)
32 | await page.goto('./partially-hydrate', { waitUntil: 'commit' })
33 | await expect(page.getByText('first data:undefined')).toBeVisible()
34 | await expect(
35 | page.getByText('second data (delayed hydration):undefined')
36 | ).toBeVisible()
37 | await expect(page.getByText('first data:SSR Works')).toBeVisible()
38 | await expect(
39 | page.getByText('second data (delayed hydration):SSR Works')
40 | ).toBeVisible()
41 | await expect(
42 | page.getByText('first history:[null,"SSR Works"]')
43 | ).toBeVisible()
44 | await expect(
45 | page.getByText('second history:[null,"SSR Works"]')
46 | ).toBeVisible()
47 | expect(log).toHaveLength(0)
48 | })
49 | })
50 |
--------------------------------------------------------------------------------
/e2e/test/suspense-fallback.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable testing-library/prefer-screen-queries */
2 | import { test, expect } from '@playwright/test'
3 |
4 | test.describe('suspense fallback', () => {
5 | test('should wait for promise fallback value to be resolved', async ({
6 | page
7 | }) => {
8 | await page.goto('./suspense-fallback/promise', { waitUntil: 'commit' })
9 | await expect(page.getByText('async promise')).toBeVisible()
10 | })
11 | })
12 |
--------------------------------------------------------------------------------
/e2e/test/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "include": [
4 | ".",
5 | ],
6 | }
--------------------------------------------------------------------------------
/examples/.eslintrc:
--------------------------------------------------------------------------------
1 | // next is loading eslintrc from the root directory, adding this to avoid eslint rules being overridden
2 | {}
3 |
--------------------------------------------------------------------------------
/examples/api-hooks/README.md:
--------------------------------------------------------------------------------
1 | # API Hooks
2 |
3 | ## One-Click Deploy
4 |
5 | Deploy your own SWR project with Vercel.
6 |
7 | [](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/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/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/examples/autocomplete-suggestions/README.md:
--------------------------------------------------------------------------------
1 | # Autocomplete Suggestions
2 |
3 | ## One-Click Deploy
4 |
5 | Deploy your own SWR project with Vercel.
6 |
7 | [](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/autocomplete-suggestions/libs/fetcher.js:
--------------------------------------------------------------------------------
1 | export default async function fetcher(...args) {
2 | const res = await fetch(...args)
3 | return res.json()
4 | }
5 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/axios-typescript/README.md:
--------------------------------------------------------------------------------
1 | # Axios TypeScript
2 |
3 | ## One-Click Deploy
4 |
5 | Deploy your own SWR project with Vercel.
6 |
7 | [](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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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-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/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/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/axios/README.md:
--------------------------------------------------------------------------------
1 | # Axios
2 |
3 | ## One-Click Deploy
4 |
5 | Deploy your own SWR project with Vercel.
6 |
7 | [](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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/examples/basic-typescript/README.md:
--------------------------------------------------------------------------------
1 | # Basic TypeScript
2 |
3 | ## One-Click Deploy
4 |
5 | Deploy your own SWR project with Vercel.
6 |
7 | [](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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/examples/basic/README.md:
--------------------------------------------------------------------------------
1 | # Basic
2 |
3 | ## One-Click Deploy
4 |
5 | Deploy your own SWR project with Vercel.
6 |
7 | [](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/basic/libs/fetch.js:
--------------------------------------------------------------------------------
1 | export default async function fetcher(...args) {
2 | const res = await fetch(...args)
3 | return res.json()
4 | }
5 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/focus-revalidate/README.md:
--------------------------------------------------------------------------------
1 | # Focus Revalidate
2 |
3 | ## One-Click Deploy
4 |
5 | Deploy your own SWR project with Vercel.
6 |
7 | [](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 |
--------------------------------------------------------------------------------
/examples/focus-revalidate/components/button.js:
--------------------------------------------------------------------------------
1 | export default function Button({ children, ...props }) {
2 | return
3 | {children}
4 |
15 |
16 | }
--------------------------------------------------------------------------------
/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/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/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/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/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 |
{
16 | logout()
17 | mutate() // after logging in/out, we mutate the SWR
18 | }}>Logout
19 |
20 | } else {
21 | return
22 |
Please login
23 | {
24 | login()
25 | mutate()
26 | }}>Login
27 |
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/examples/global-fetcher/README.md:
--------------------------------------------------------------------------------
1 | # Global Fetcher
2 |
3 | ## One-Click Deploy
4 |
5 | Deploy your own SWR project with Vercel.
6 |
7 | [](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/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/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/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 |
--------------------------------------------------------------------------------
/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/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/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/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 | [](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/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 |
--------------------------------------------------------------------------------
/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/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/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 |
{
53 | setRepo(val)
54 | setSize(1)
55 | }}
56 | >
57 | load issues
58 |
59 |
60 | showing {size} page(s) of {isLoadingMore ? '...' : issues.length}{' '}
61 | issue(s){' '}
62 | mutate()}>
63 | {isRefreshing ? 'refreshing...' : 'refresh'}
64 |
65 | setSize(0)}>
66 | clear
67 |
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 |
--------------------------------------------------------------------------------
/examples/infinite/README.md:
--------------------------------------------------------------------------------
1 | # useSWRInfinite
2 |
3 | ## One-Click Deploy
4 |
5 | Deploy your own SWR project with Vercel.
6 |
7 | [](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/infinite/libs/fetch.js:
--------------------------------------------------------------------------------
1 | export default async function fetcher(...args) {
2 | const res = await fetch(...args)
3 | return res.json()
4 | }
5 |
--------------------------------------------------------------------------------
/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/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 |
{
39 | setRepo(val)
40 | setSize(1)
41 | }}
42 | >
43 | load issues
44 |
45 |
46 | showing {size} page(s) of {isLoadingMore ? '...' : issues.length}{' '}
47 | issue(s){' '}
48 | setSize(size + 1)}
51 | >
52 | {isLoadingMore
53 | ? 'loading...'
54 | : isReachingEnd
55 | ? 'no more issues'
56 | : 'load more'}
57 |
58 | mutate()}>
59 | {isRefreshing ? 'refreshing...' : 'refresh'}
60 |
61 | setSize(0)}>
62 | clear
63 |
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 |
--------------------------------------------------------------------------------
/examples/local-state-sharing/README.md:
--------------------------------------------------------------------------------
1 | # Basic
2 |
3 | ## One-Click Deploy
4 |
5 | Deploy your own SWR project with Vercel.
6 |
7 | [](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/local-state-sharing/libs/store.js:
--------------------------------------------------------------------------------
1 | const initialStore = { name: "john" };
2 |
3 | export default initialStore;
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | {
22 | mutate("globalState", { ...data, name: value }, false)
23 | }}
24 | >
25 | Uppercase my name!
26 |
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 |
--------------------------------------------------------------------------------
/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 | [](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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
42 |
43 | {data ? data.map(datum => {datum} ) : 'loading...'}
44 |
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/examples/optimistic-ui/README.md:
--------------------------------------------------------------------------------
1 | # Optimistic UI
2 |
3 | ## One-Click Deploy
4 |
5 | Deploy your own SWR project with Vercel.
6 |
7 | [](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/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/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/optimistic-ui/pages/_app.js:
--------------------------------------------------------------------------------
1 | import '../styles.css'
2 |
3 | export default function App({ Component, pageProps }) {
4 | return
5 | }
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/prefetch-preload/README.md:
--------------------------------------------------------------------------------
1 | # Prefetch & Preload
2 |
3 | ## One-Click Deploy
4 |
5 | Deploy your own SWR project with Vercel.
6 |
7 | [](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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | [](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/refetch-interval/components/button.js:
--------------------------------------------------------------------------------
1 | export default function Button({ children, ...props }) {
2 | return
3 | {children}
4 |
15 |
16 | }
--------------------------------------------------------------------------------
/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/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/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/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 |
28 |
29 | {data.map(item => {item} )}
30 |
31 |
{
32 | await fetch(`/api/data?clear=1`)
33 | mutate()
34 | }}>Clear All
35 |
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/examples/server-render/README.md:
--------------------------------------------------------------------------------
1 | # Server Render
2 |
3 | ## One-Click Deploy
4 |
5 | Deploy your own SWR project with Vercel.
6 |
7 | [](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 |
--------------------------------------------------------------------------------
/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/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/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/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 | }
--------------------------------------------------------------------------------
/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 | [](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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | Name
18 |
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/examples/subscription/README.md:
--------------------------------------------------------------------------------
1 | # Subscription
2 |
3 | ## One-Click Deploy
4 |
5 | Deploy your own SWR project with Vercel.
6 |
7 | [](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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
{
18 | resetErrorBoundary()
19 | }}
20 | >
21 | retry
22 |
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-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 |
--------------------------------------------------------------------------------
/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/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/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | experimental: {
4 | serverActions: true,
5 | },
6 | }
7 |
8 | module.exports = nextConfig
9 |
--------------------------------------------------------------------------------
/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": "^13.4.4",
16 | "react": "^18.2.0",
17 | "react-dom": "^18.2.0",
18 | "typescript": "5.1.3",
19 | "swr": "*"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/examples/suspense/README.md:
--------------------------------------------------------------------------------
1 | # Basic
2 |
3 | ## One-Click Deploy
4 |
5 | Deploy your own SWR project with Vercel.
6 |
7 | [](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/layout.jsx:
--------------------------------------------------------------------------------
1 | export default function RootLayout({
2 | children
3 | }) {
4 | return (
5 |
6 | {children}
7 |
8 | )
9 | }
10 |
--------------------------------------------------------------------------------
/examples/suspense/app/rsc/[user]/[repo]/error.jsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | export default function ErrorPage() {
3 | return Error happen
;
4 | }
--------------------------------------------------------------------------------
/examples/suspense/app/rsc/[user]/[repo]/loading.jsx:
--------------------------------------------------------------------------------
1 | export default function Loading() {
2 | return Loading...
;
3 | }
--------------------------------------------------------------------------------
/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/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/suspense/app/rsc/loading.jsx:
--------------------------------------------------------------------------------
1 | export default function Loading() {
2 | return Loading...
;
3 | }
--------------------------------------------------------------------------------
/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/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/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/libs/fetch.js:
--------------------------------------------------------------------------------
1 | export default async function fetcher(...args) {
2 | const res = await fetch(...args)
3 | return res.json()
4 | }
5 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - '_internal'
3 | - 'core'
4 | - 'immutable'
5 | - 'infinite'
6 | - 'mutation'
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/_internal/constants.ts:
--------------------------------------------------------------------------------
1 | export const INFINITE_PREFIX = '$inf$'
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/_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 |
--------------------------------------------------------------------------------
/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 | cacheContext[2] && cacheContext[2]()
63 | return cacheContext[3]
64 | }
65 | }, [])
66 |
67 | return createElement(
68 | SWRConfigContext.Provider,
69 | mergeObjects(props, {
70 | value: extendedConfig
71 | })
72 | )
73 | }
74 |
75 | export default SWRConfig
76 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 isObjectType = (value: any, type: string) =>
10 | OBJECT.prototype.toString.call(value) === `[object ${type}]`
11 |
12 | // counter of the key
13 | let counter = 0
14 |
15 | // A stable hash implementation that supports:
16 | // - Fast and ensures unique hash properties
17 | // - Handles unserializable values
18 | // - Handles object key ordering
19 | // - Generates short results
20 | //
21 | // This is not a serialization function, and the result is not guaranteed to be
22 | // parsable.
23 | export const stableHash = (arg: any): string => {
24 | const type = typeof arg
25 | const isDate = isObjectType(arg, 'Date')
26 | const isRegex = isObjectType(arg, 'RegExp')
27 | const isPlainObject = isObjectType(arg, 'Object')
28 | let result: any
29 | let index: any
30 |
31 | if (OBJECT(arg) === arg && !isDate && !isRegex) {
32 | // Object/function, not null/date/regexp. Use WeakMap to store the id first.
33 | // If it's already hashed, directly return the result.
34 | result = table.get(arg)
35 | if (result) return result
36 |
37 | // Store the hash first for circular reference detection before entering the
38 | // recursive `stableHash` calls.
39 | // For other objects like set and map, we use this id directly as the hash.
40 | result = ++counter + '~'
41 | table.set(arg, result)
42 |
43 | if (Array.isArray(arg)) {
44 | // Array.
45 | result = '@'
46 | for (index = 0; index < arg.length; index++) {
47 | result += stableHash(arg[index]) + ','
48 | }
49 | table.set(arg, result)
50 | }
51 | if (isPlainObject) {
52 | // Object, sort keys.
53 | result = '#'
54 | const keys = OBJECT.keys(arg).sort()
55 | while (!isUndefined((index = keys.pop() as string))) {
56 | if (!isUndefined(arg[index])) {
57 | result += index + ':' + stableHash(arg[index]) + ','
58 | }
59 | }
60 | table.set(arg, result)
61 | }
62 | } else {
63 | result = isDate
64 | ? arg.toJSON()
65 | : type == 'symbol'
66 | ? arg.toString()
67 | : type == 'string'
68 | ? JSON.stringify(arg)
69 | : '' + arg
70 | }
71 |
72 | return result
73 | }
74 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/_internal/utils/timestamp.ts:
--------------------------------------------------------------------------------
1 | // Global timestamp.
2 | let __timestamp = 0
3 |
4 | export const getTimestamp = () => ++__timestamp
5 |
--------------------------------------------------------------------------------
/src/_internal/utils/use-swr-config.ts:
--------------------------------------------------------------------------------
1 | import { useContext } 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 | return mergeObjects(defaultConfig, useContext(SWRConfigContext))
9 | }
10 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/index/index.react-server.ts:
--------------------------------------------------------------------------------
1 | export { unstable_serialize } from './serialize'
2 | export { SWRConfig } from './config'
3 |
--------------------------------------------------------------------------------
/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 | // Types
12 | export type {
13 | SWRConfiguration,
14 | Revalidator,
15 | RevalidatorOptions,
16 | Key,
17 | KeyLoader,
18 | KeyedMutator,
19 | SWRHook,
20 | SWRResponse,
21 | Cache,
22 | BareFetcher,
23 | Fetcher,
24 | MutatorCallback,
25 | MutatorOptions,
26 | Middleware,
27 | Arguments,
28 | State,
29 | ScopedMutator
30 | } from '../_internal'
31 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/infinite/index.react-server.ts:
--------------------------------------------------------------------------------
1 | export { unstable_serialize } from './serialize'
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
67 | currentState[k] = payload[k]!
68 |
69 | // If the property is accessed by the component, a rerender should be
70 | // triggered.
71 | if (stateDependenciesRef.current[k]) {
72 | shouldRerender = true
73 | }
74 | }
75 | }
76 | }
77 |
78 | if (shouldRerender && !unmountedRef.current) {
79 | rerender({})
80 | }
81 | }, [])
82 |
83 | useIsomorphicLayoutEffect(() => {
84 | unmountedRef.current = false
85 | return () => {
86 | unmountedRef.current = true
87 | }
88 | })
89 |
90 | return [stateRef, stateDependenciesRef.current, setState]
91 | }
92 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/test/jest-setup.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom'
2 |
--------------------------------------------------------------------------------
/test/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "strict": false,
5 | "jsx": "react-jsx",
6 | "baseUrl": "..",
7 | "paths": {
8 | "swr": ["./core/src/index.ts"],
9 | "swr/infinite": ["./infinite/src/index.ts"],
10 | "swr/immutable": ["./immutable/src/index.ts"],
11 | "swr/mutation": ["./mutation/src/index.ts"],
12 | "swr/_internal": ["./_internal/src/index.ts"],
13 | "swr/subscription": ["subscription/src/index.ts"],
14 | },
15 | },
16 | "include": [".", "./jest-setup.ts"],
17 | "exclude": ["./type"]
18 | }
19 |
--------------------------------------------------------------------------------
/test/type/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../.eslintrc",
3 | "rules": {
4 | "react-hooks/rules-of-hooks": 0
5 | }
6 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/mutate.ts:
--------------------------------------------------------------------------------
1 | import type { Equal, Expect } from '@type-challenges/utils'
2 | import useSWR, { useSWRConfig } from 'swr'
3 | import type {
4 | MutatorFn,
5 | Key,
6 | MutatorCallback,
7 | Mutator,
8 | MutatorWrapper,
9 | Arguments
10 | } from 'swr/_internal'
11 | import { expectType } from './utils'
12 |
13 | type Case1 = MutatorFn
14 | type Case2 = (
15 | cache: Cache,
16 | key: Key,
17 | data: Data | Promise | MutatorCallback,
18 | opts: boolean
19 | ) => Promise
20 | type Case3 = (
21 | cache: Cache,
22 | key: Key,
23 | data: Data | Promise | MutatorCallback
24 | ) => Promise
25 | type Case4 = (
26 | cache: Cache,
27 | key: Key,
28 | data: Data | Promise | MutatorCallback,
29 | opts: {
30 | populateCache: undefined
31 | }
32 | ) => Promise
33 | type Case5 = (
34 | cache: Cache,
35 | key: Key,
36 | data: Data | Promise | MutatorCallback,
37 | opts: {
38 | populateCache: false
39 | }
40 | ) => Promise
41 | type Case6 = (
42 | cache: Cache,
43 | key: Key,
44 | data: Data | Promise | MutatorCallback,
45 | opts: {
46 | populateCache: true
47 | }
48 | ) => Promise
49 |
50 | export type TestCasesForMutator = [
51 | Expect, Promise<{} | undefined>>>,
52 | Expect>, Promise<{} | undefined>>>,
53 | Expect>, Promise<{} | undefined>>>,
54 | Expect>, Promise<{} | undefined>>>,
55 | Expect>, Promise<{} | undefined>>>,
56 | Expect>, never>>,
57 | Expect>, Promise<{} | undefined>>>
58 | ]
59 |
60 | export function useMutatorTypes() {
61 | const { mutate } = useSWR('')
62 |
63 | mutate(async () => '1')
64 | mutate(async () => '1', { populateCache: false })
65 |
66 | // @ts-expect-error
67 | mutate(async () => 1)
68 | // @ts-expect-error
69 | mutate(async () => 1, { populateCache: false })
70 | }
71 |
72 | export function useConfigMutate() {
73 | const { mutate } = useSWRConfig()
74 | expect>>(
75 | mutate(
76 | key => {
77 | expectType(key)
78 | return typeof key === 'string' && key.startsWith('swr')
79 | },
80 | data => {
81 | expectType(data)
82 | return 0
83 | }
84 | )
85 | )
86 |
87 | expect>(
88 | mutate('string', (data?: string) => {
89 | expectType(data)
90 | return '0'
91 | })
92 | )
93 |
94 | expect>>(
95 | mutate(
96 | key => {
97 | expectType(key)
98 | return typeof key === 'string' && key.startsWith('swr')
99 | },
100 | data => {
101 | expectType(data)
102 | return 0
103 | }
104 | )
105 | )
106 |
107 | expect>(
108 | mutate('string', data => {
109 | expectType(data)
110 | return '0'
111 | })
112 | )
113 |
114 | mutate('string', data => {
115 | expectType(data)
116 | return '0'
117 | })
118 | }
119 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/test/type/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "strict": true,
5 | "jsx": "react-jsx",
6 | },
7 | "include": ["./**/*.ts", "./**/*.tsx"],
8 | }
9 |
--------------------------------------------------------------------------------
/test/type/utils.ts:
--------------------------------------------------------------------------------
1 | export type ExpectType = (value: T) => void
2 | export const expectType: ExpectType = () => {}
3 |
4 | export const truthy: () => boolean = () => true
5 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/test/unit/web-preset.test.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from 'events'
2 |
3 | const FOCUS_EVENT = 'focus'
4 | const VISIBILITYCHANGE_EVENT = 'visibilitychange'
5 |
6 | function createEventTarget() {
7 | EventEmitter.prototype['addEventListener'] = EventEmitter.prototype.on
8 | EventEmitter.prototype['removeEventListener'] = EventEmitter.prototype.off
9 | const target = new EventEmitter()
10 |
11 | return target
12 | }
13 |
14 | function runTests(propertyName) {
15 | let initFocus
16 | const eventName =
17 | propertyName === 'window' ? FOCUS_EVENT : VISIBILITYCHANGE_EVENT
18 |
19 | describe(`Web Preset ${propertyName}`, () => {
20 | const globalSpy = {
21 | window: undefined,
22 | document: undefined
23 | }
24 |
25 | beforeEach(() => {
26 | globalSpy.window = jest.spyOn(global, 'window', 'get')
27 | globalSpy.document = jest.spyOn(global, 'document', 'get')
28 |
29 | jest.resetModules()
30 | })
31 |
32 | afterEach(() => {
33 | globalSpy.window.mockClear()
34 | globalSpy.document.mockClear()
35 | })
36 |
37 | it(`should trigger listener when ${propertyName} has browser APIs`, async () => {
38 | const target = createEventTarget()
39 | if (propertyName === 'window') {
40 | globalSpy.window.mockImplementation(() => target)
41 | globalSpy.document.mockImplementation(() => undefined)
42 | } else if (propertyName === 'document') {
43 | globalSpy.window.mockImplementation(() => undefined)
44 | globalSpy.document.mockImplementation(() => target)
45 | }
46 |
47 | initFocus = require('swr/_internal').defaultConfigOptions.initFocus
48 |
49 | const fn = jest.fn()
50 | const release = initFocus(fn) as () => void
51 |
52 | target.emit(eventName)
53 | expect(fn).toBeCalledTimes(1)
54 |
55 | release()
56 | target.emit(eventName)
57 | expect(fn).toBeCalledTimes(1)
58 | })
59 |
60 | it(`should not trigger listener when ${propertyName} is falsy`, async () => {
61 | if (propertyName === 'window') {
62 | // window exists but without event APIs
63 | globalSpy.window.mockImplementation(() => ({
64 | emit: createEventTarget().emit
65 | }))
66 | globalSpy.document.mockImplementation(() => undefined)
67 | } else if (propertyName === 'document') {
68 | globalSpy.window.mockImplementation(() => undefined)
69 | globalSpy.document.mockImplementation(() => undefined)
70 | }
71 |
72 | initFocus = require('swr/_internal').defaultConfigOptions.initFocus
73 |
74 | const fn = jest.fn()
75 | const release = initFocus(fn) as () => void
76 | const target = global[propertyName]
77 |
78 | target?.emit?.(eventName)
79 |
80 | expect(fn).toBeCalledTimes(0)
81 |
82 | release()
83 | if (target && target.emit) {
84 | target.emit(eventName)
85 | }
86 | expect(fn).toBeCalledTimes(0)
87 | })
88 | })
89 | }
90 |
91 | runTests('window')
92 | runTests('document')
93 |
--------------------------------------------------------------------------------
/test/use-swr-concurrent-rendering.test.tsx:
--------------------------------------------------------------------------------
1 | import { screen, fireEvent, act } from '@testing-library/react'
2 | import {
3 | createKey,
4 | createResponse,
5 | sleep,
6 | executeWithoutBatching,
7 | renderWithConfig
8 | } from './utils'
9 | import React 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 | it('should pause when changing the key inside a transition', async () => {
31 | const initialKey = createKey()
32 | const newKey = createKey()
33 | const fetcher = (k: string) => createResponse(k, { delay: 100 })
34 | // eslint-disable-next-line react/prop-types
35 | function Component({ swrKey }) {
36 | const { data } = useSWR(swrKey, fetcher, {
37 | dedupingInterval: 0,
38 | suspense: true
39 | })
40 |
41 | return <>data:{data}>
42 | }
43 | function Page() {
44 | const [isPending, startTransition] = React.useTransition()
45 | const [key, setKey] = React.useState(initialKey)
46 |
47 | return (
48 | startTransition(() => setKey(newKey))}>
49 | isPending:{isPending ? 1 : 0},
50 |
51 |
52 |
53 |
54 | )
55 | }
56 |
57 | renderWithConfig( )
58 |
59 | screen.getByText('isPending:0,loading')
60 | await act(() => sleep(120))
61 | screen.getByText(`isPending:0,data:${initialKey}`)
62 | fireEvent.click(screen.getByText(`isPending:0,data:${initialKey}`))
63 | await act(() => sleep(10))
64 |
65 | // Pending state
66 | screen.getByText(`isPending:1,data:${initialKey}`)
67 |
68 | // Transition end
69 | await act(() => sleep(120))
70 | screen.getByText(`isPending:0,data:${newKey}`)
71 | })
72 |
73 | // https://codesandbox.io/s/concurrent-swr-case-ii-lr6x4u
74 | it.skip('should do state updates in transitions', async () => {
75 | const key1 = createKey()
76 | const key2 = createKey()
77 |
78 | const log = []
79 |
80 | function Counter() {
81 | const [count, setCount] = React.useState(0)
82 |
83 | React.useEffect(() => {
84 | const interval = setInterval(() => {
85 | setCount(x => x + 1)
86 | }, 20)
87 | return () => clearInterval(interval)
88 | }, [])
89 |
90 | log.push(count)
91 |
92 | return <>{count}>
93 | }
94 |
95 | function Body() {
96 | useSWR(key2, () => createResponse(true, { delay: 1000 }), {
97 | revalidateOnFocus: false,
98 | revalidateOnReconnect: false,
99 | dedupingInterval: 0,
100 | suspense: true
101 | })
102 | return null
103 | }
104 |
105 | function Page() {
106 | const { data } = useSWR(key1, () => createResponse(true, { delay: 50 }), {
107 | revalidateOnFocus: false,
108 | revalidateOnReconnect: false,
109 | dedupingInterval: 0
110 | })
111 |
112 | return (
113 | <>
114 |
115 | {data ? : null}
116 | >
117 | )
118 | }
119 |
120 | await executeWithoutBatching(async () => {
121 | renderWithConfig( )
122 | await sleep(500)
123 | })
124 | })
125 | })
126 |
--------------------------------------------------------------------------------
/test/use-swr-context-config.test.tsx:
--------------------------------------------------------------------------------
1 | import { act, screen } from '@testing-library/react'
2 | import useSWR, { mutate } from 'swr'
3 | import { createKey, createResponse, renderWithGlobalCache } from './utils'
4 |
5 | describe('useSWR - context configs', () => {
6 | it('mutate before mount should not block rerender', async () => {
7 | const prefetch = () => createResponse('prefetch-data')
8 | const fetcher = () => createResponse('data')
9 | const key = createKey()
10 |
11 | await act(async () => {
12 | await mutate(key, prefetch)
13 | })
14 |
15 | function Page() {
16 | const { data } = useSWR(key, fetcher)
17 | return {data}
18 | }
19 |
20 | renderWithGlobalCache( )
21 | // render with the prefetched data
22 | screen.getByText('prefetch-data')
23 |
24 | // render the fetched data
25 | await screen.findByText('data')
26 | })
27 | })
28 |
--------------------------------------------------------------------------------
/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 | ;({ createKey, createResponse, renderWithConfig } = require('./utils'))
16 | useSWR = require('swr').default
17 | })
18 | it('window.__SWR_DEVTOOLS_USE__ should be set as middleware', async () => {
19 | const key = createKey()
20 | function Page() {
21 | const { data } = useSWR(key, () => createResponse('ok'))
22 | return data: {data}
23 | }
24 | renderWithConfig( )
25 | await screen.findByText('data: middleware')
26 | })
27 | it('window.__SWR_DEVTOOLS_REACT__ should be the same reference with React', () => {
28 | // @ts-expect-error
29 | expect(window.__SWR_DEVTOOLS_REACT__).toBe(React)
30 | })
31 | })
32 |
--------------------------------------------------------------------------------
/test/use-swr-legacy-react.test.tsx:
--------------------------------------------------------------------------------
1 | // This test case covers special environments such as React <= 17.
2 |
3 | import { act, screen, render, fireEvent } from '@testing-library/react'
4 |
5 | // https://github.com/jestjs/jest/issues/11471
6 | jest.mock('react', () => jest.requireActual('react'))
7 |
8 | async function withLegacyReact(runner: () => Promise) {
9 | await jest.isolateModulesAsync(async () => {
10 | await runner()
11 | })
12 | }
13 |
14 | describe('useSWR - legacy React', () => {
15 | ;(process.env.__SWR_TEST_BUILD ? it.skip : it)(
16 | 'should enable the IS_REACT_LEGACY flag - startTransition',
17 | async () => {
18 | await withLegacyReact(async () => {
19 | // Test mutation and trigger
20 | const useSWRMutation = (await import('swr/mutation')).default
21 |
22 | const waitForNextTick = () =>
23 | act(() => new Promise(resolve => setTimeout(resolve, 1)))
24 | const key = Math.random().toString()
25 |
26 | function Page() {
27 | const { data, trigger } = useSWRMutation(key, () => 'data')
28 | return trigger()}>{data || 'pending'}
29 | }
30 |
31 | render( )
32 |
33 | // mount
34 | await screen.findByText('pending')
35 |
36 | fireEvent.click(screen.getByText('pending'))
37 | await waitForNextTick()
38 |
39 | screen.getByText('data')
40 | })
41 | }
42 | )
43 |
44 | // https://github.com/vercel/swr/blob/cfcfa9e320a59742d41a77e52003127b04378c4f/src/core/use-swr.ts#L345
45 | ;(process.env.__SWR_TEST_BUILD ? it.skip : it)(
46 | 'should enable the IS_REACT_LEGACY flag - unmount check',
47 | async () => {
48 | await withLegacyReact(async () => {
49 | const useSWR = (await import('swr')).default
50 |
51 | const key = Math.random().toString()
52 |
53 | function Page() {
54 | // No fallback data
55 | const { data } = useSWR(
56 | key,
57 | () =>
58 | new Promise(resolve =>
59 | setTimeout(() => resolve('data'), 100)
60 | ),
61 | {
62 | loadingTimeout: 10
63 | }
64 | )
65 | return {data || 'pending'}
66 | }
67 |
68 | render( )
69 |
70 | await screen.findByText('data')
71 | })
72 | }
73 | )
74 | })
75 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/test/use-swr-offline.test.tsx:
--------------------------------------------------------------------------------
1 | import { act, screen } from '@testing-library/react'
2 | import useSWR from 'swr'
3 | import {
4 | nextTick as waitForNextTick,
5 | focusOn,
6 | createKey,
7 | renderWithConfig
8 | } from './utils'
9 |
10 | const focusWindow = () => focusOn(window)
11 | const dispatchWindowEvent = event =>
12 | act(async () => {
13 | window.dispatchEvent(new Event(event))
14 | })
15 |
16 | describe('useSWR - offline', () => {
17 | it('should not revalidate when offline', async () => {
18 | let value = 0
19 |
20 | const key = createKey()
21 | function Page() {
22 | const { data } = useSWR(key, () => value++, {
23 | dedupingInterval: 0
24 | })
25 | return data: {data}
26 | }
27 |
28 | renderWithConfig( )
29 | // hydration
30 | screen.getByText('data:')
31 | // mount
32 | await screen.findByText('data: 0')
33 |
34 | // simulate offline
35 | await waitForNextTick()
36 | await dispatchWindowEvent('offline')
37 |
38 | // trigger focus revalidation
39 | await focusWindow()
40 |
41 | // should not be revalidated
42 | screen.getByText('data: 0')
43 | })
44 |
45 | it('should revalidate immediately when becoming online', async () => {
46 | let value = 0
47 |
48 | const key = createKey()
49 | function Page() {
50 | const { data } = useSWR(key, () => value++, {
51 | dedupingInterval: 0
52 | })
53 | return data: {data}
54 | }
55 |
56 | renderWithConfig( )
57 | // hydration
58 | screen.getByText('data:')
59 | // mount
60 | await screen.findByText('data: 0')
61 |
62 | // simulate online
63 | await waitForNextTick()
64 | await dispatchWindowEvent('online')
65 |
66 | // should be revalidated
67 | await screen.findByText('data: 1')
68 | })
69 | })
70 |
--------------------------------------------------------------------------------
/test/use-swr-reconnect.test.tsx:
--------------------------------------------------------------------------------
1 | import { screen, fireEvent, createEvent } from '@testing-library/react'
2 | import useSWR from 'swr'
3 | import {
4 | nextTick as waitForNextTick,
5 | renderWithConfig,
6 | createKey,
7 | mockVisibilityHidden
8 | } from './utils'
9 |
10 | describe('useSWR - reconnect', () => {
11 | it('should revalidate on reconnect by default', async () => {
12 | let value = 0
13 | const key = createKey()
14 | function Page() {
15 | const { data } = useSWR(key, () => value++, {
16 | dedupingInterval: 0
17 | })
18 | return data: {data}
19 | }
20 |
21 | renderWithConfig( )
22 | // hydration
23 | screen.getByText('data:')
24 | // mount
25 | await screen.findByText('data: 0')
26 |
27 | await waitForNextTick()
28 |
29 | // trigger reconnect
30 | fireEvent(window, createEvent('offline', window))
31 | fireEvent(window, createEvent('online', window))
32 |
33 | await screen.findByText('data: 1')
34 | })
35 |
36 | it("shouldn't revalidate on reconnect when revalidateOnReconnect is false", async () => {
37 | let value = 0
38 |
39 | const key = createKey()
40 | function Page() {
41 | const { data } = useSWR(key, () => value++, {
42 | dedupingInterval: 0,
43 | revalidateOnReconnect: false
44 | })
45 | return data: {data}
46 | }
47 |
48 | renderWithConfig( )
49 | // hydration
50 | screen.getByText('data:')
51 |
52 | // mount
53 | await screen.findByText('data: 0')
54 |
55 | await waitForNextTick()
56 |
57 | // trigger reconnect
58 | fireEvent(window, createEvent('offline', window))
59 | fireEvent(window, createEvent('online', window))
60 |
61 | // should not be revalidated
62 | screen.getByText('data: 0')
63 | })
64 |
65 | it("shouldn't revalidate on reconnect when isOnline is returning false", async () => {
66 | let value = 0
67 |
68 | const key = createKey()
69 | function Page() {
70 | const { data } = useSWR(key, () => value++, {
71 | dedupingInterval: 0,
72 | isOnline: () => false
73 | })
74 | return data: {data}
75 | }
76 |
77 | renderWithConfig( )
78 | // hydration
79 | screen.getByText('data:')
80 |
81 | // mount
82 | await screen.findByText('data: 0')
83 |
84 | await waitForNextTick()
85 |
86 | // trigger reconnect
87 | fireEvent(window, createEvent('offline', window))
88 | fireEvent(window, createEvent('online', window))
89 |
90 | // should not be revalidated
91 | screen.getByText('data: 0')
92 | })
93 |
94 | it("shouldn't revalidate on reconnect if invisible", async () => {
95 | let value = 0
96 |
97 | const key = createKey()
98 | function Page() {
99 | const { data } = useSWR(key, () => value++, {
100 | dedupingInterval: 0,
101 | isOnline: () => false
102 | })
103 | return data: {data}
104 | }
105 |
106 | renderWithConfig( )
107 | // hydration
108 | screen.getByText('data:')
109 |
110 | // mount
111 | await screen.findByText('data: 0')
112 |
113 | await waitForNextTick()
114 |
115 | const resetVisibility = mockVisibilityHidden()
116 |
117 | // trigger reconnect
118 | fireEvent(window, createEvent('offline', window))
119 | fireEvent(window, createEvent('online', window))
120 |
121 | // should not be revalidated
122 | screen.getByText('data: 0')
123 |
124 | resetVisibility()
125 | })
126 | })
127 |
--------------------------------------------------------------------------------
/test/use-swr-server.test.tsx:
--------------------------------------------------------------------------------
1 | // This test case covers special environments such as React <= 17 and SSR.
2 |
3 | import { screen, render } from '@testing-library/react'
4 | import { Suspense } from 'react'
5 | import { ErrorBoundary } from 'react-error-boundary'
6 |
7 | // https://github.com/jestjs/jest/issues/11471
8 | jest.mock('react', () => jest.requireActual('react'))
9 |
10 | async function withServer(runner: () => Promise) {
11 | await jest.isolateModulesAsync(async () => {
12 | await runner()
13 | })
14 | }
15 |
16 | describe('useSWR - SSR', () => {
17 | beforeAll(() => {
18 | // Store the original window object
19 | // @ts-expect-error
20 | global.window.Deno = '1'
21 |
22 | // Mock window to undefined
23 | // delete global.window;
24 | })
25 |
26 | afterAll(() => {
27 | // Restore window back to its original value
28 | // @ts-expect-error
29 | delete global.window.Deno
30 | })
31 | it('should enable the IS_SERVER flag - suspense on server without fallback', async () => {
32 | await withServer(async () => {
33 | // eslint-disable-next-line @typescript-eslint/no-empty-function
34 | jest.spyOn(console, 'error').mockImplementation(() => {})
35 | const useSWR = (await import('swr')).default
36 |
37 | const key = Math.random().toString()
38 |
39 | const Page = () => {
40 | const { data } = useSWR(key, () => 'SWR', {
41 | suspense: true
42 | })
43 | return {data || 'empty'}
44 | }
45 |
46 | render(
47 | {
49 | console.error(error)
50 | return {error.message}
51 | }}
52 | >
53 |
54 |
55 |
56 |
57 | )
58 |
59 | await screen.findByText(
60 | 'Fallback data is required when using Suspense in SSR.'
61 | )
62 | })
63 | })
64 | })
65 |
--------------------------------------------------------------------------------
/test/use-swr-streaming-ssr.test.tsx:
--------------------------------------------------------------------------------
1 | import { act } from '@testing-library/react'
2 | import { Suspense } from 'react'
3 | import useSWR from 'swr'
4 | import {
5 | createKey,
6 | createResponse,
7 | renderWithConfig,
8 | hydrateWithConfig,
9 | mockConsoleForHydrationErrors,
10 | sleep
11 | } from './utils'
12 |
13 | describe('useSWR - streaming', () => {
14 | afterEach(() => {
15 | jest.clearAllMocks()
16 | jest.restoreAllMocks()
17 | })
18 |
19 | it('should match ssr result when hydrating', async () => {
20 | const ensureAndUnmock = mockConsoleForHydrationErrors()
21 |
22 | const key = createKey()
23 |
24 | // A block fetches the data and updates the cache.
25 | function Block() {
26 | const { data } = useSWR(key, () => createResponse('SWR', { delay: 10 }))
27 | return {data || 'undefined'}
28 | }
29 |
30 | const container = document.createElement('div')
31 | container.innerHTML = 'undefined
'
32 | await hydrateWithConfig( , container)
33 | ensureAndUnmock()
34 | })
35 |
36 | // NOTE: this test is failing because it's not possible to test this behavior
37 | // in JSDOM. We need to test this in a real browser.
38 | it.failing(
39 | 'should match the ssr result when streaming and partially hydrating',
40 | async () => {
41 | const key = createKey()
42 |
43 | const dataDuringHydration = {}
44 |
45 | // A block fetches the data and updates the cache.
46 | function Block({ suspense, delay, id }) {
47 | const { data } = useSWR(key, () => createResponse('SWR', { delay }), {
48 | suspense
49 | })
50 |
51 | // The first render is always hydration in our case.
52 | if (!dataDuringHydration[id]) {
53 | dataDuringHydration[id] = data || 'undefined'
54 | }
55 |
56 | return {data || 'undefined'}
57 | }
58 |
59 | // In this example, a will be hydrated first and b will still be streamed.
60 | // When a is hydrated, it will update the client cache to SWR, and when
61 | // b is being hydrated, it should NOT read that cache.
62 | renderWithConfig(
63 | <>
64 |
65 |
66 |
67 |
68 | >
69 | )
70 |
71 | // The SSR result will always be 2 undefined values because data fetching won't
72 | // happen on the server:
73 | // undefined
74 | // undefined
75 |
76 | // Wait for streaming to finish.
77 | await act(() => sleep(50))
78 |
79 | expect(dataDuringHydration).toEqual({
80 | a: 'undefined',
81 | b: 'undefined'
82 | })
83 | }
84 | )
85 | })
86 |
--------------------------------------------------------------------------------
/test/utils.tsx:
--------------------------------------------------------------------------------
1 | import { act, fireEvent, render } from '@testing-library/react'
2 | import { SWRConfig } from 'swr'
3 |
4 | export function sleep(time: number) {
5 | return new Promise(resolve => setTimeout(resolve, time))
6 | }
7 |
8 | export const createResponse = (
9 | response: T,
10 | { delay } = { delay: 10 }
11 | ): Promise =>
12 | new Promise((resolve, reject) =>
13 | setTimeout(() => {
14 | if (response instanceof Error) {
15 | reject(response)
16 | } else {
17 | resolve(response)
18 | }
19 | }, delay)
20 | )
21 |
22 | export const nextTick = () => act(() => sleep(1))
23 |
24 | export const focusOn = (element: any) =>
25 | act(async () => {
26 | fireEvent.focus(element)
27 | })
28 |
29 | export const createKey = () => 'swr-key-' + ~~(Math.random() * 1e7)
30 |
31 | const _renderWithConfig = (
32 | element: React.ReactElement,
33 | config: Parameters[0]['value']
34 | ): ReturnType => {
35 | const TestSWRConfig = ({ children }: { children: React.ReactNode }) => (
36 | {children}
37 | )
38 | return render(element, { wrapper: TestSWRConfig })
39 | }
40 |
41 | export const renderWithConfig = (
42 | element: React.ReactElement,
43 | config?: Parameters[1]
44 | ): ReturnType => {
45 | const provider = () => new Map()
46 | return _renderWithConfig(element, { provider, ...config })
47 | }
48 |
49 | export const renderWithGlobalCache = (
50 | element: React.ReactElement,
51 | config?: Parameters[1]
52 | ): ReturnType => {
53 | return _renderWithConfig(element, { ...config })
54 | }
55 |
56 | export const hydrateWithConfig = (
57 | element: React.ReactElement,
58 | container: HTMLElement,
59 | config?: Parameters[1]
60 | ): ReturnType => {
61 | const provider = () => new Map()
62 | const TestSWRConfig = ({ children }: { children: React.ReactNode }) => (
63 | {children}
64 | )
65 | return render(element, {
66 | container,
67 | wrapper: TestSWRConfig,
68 | hydrate: true,
69 | legacyRoot: process.env.TEST_REACT_LEGACY === '1'
70 | })
71 | }
72 |
73 | export const mockVisibilityHidden = () => {
74 | const mockVisibilityState = jest.spyOn(document, 'visibilityState', 'get')
75 | mockVisibilityState.mockImplementation(() => 'hidden')
76 | return () => mockVisibilityState.mockRestore()
77 | }
78 |
79 | // Using `act()` will cause React 18 to batch updates.
80 | // https://github.com/reactwg/react-18/discussions/102
81 | export async function executeWithoutBatching(fn: () => any) {
82 | const prev = global.IS_REACT_ACT_ENVIRONMENT
83 | global.IS_REACT_ACT_ENVIRONMENT = false
84 | await fn()
85 | global.IS_REACT_ACT_ENVIRONMENT = prev
86 | }
87 |
88 | export const mockConsoleForHydrationErrors = () => {
89 | jest.spyOn(console, 'error').mockImplementation(() => {})
90 | return () => {
91 | // It should not have any hydration warnings.
92 | expect(
93 | // @ts-expect-error
94 | console.error.mock.calls.find(([err]) => {
95 | return (
96 | err?.message?.includes(
97 | 'Text content does not match server-rendered HTML.'
98 | ) ||
99 | err?.message?.includes(
100 | 'Hydration failed because the initial UI does not match what was rendered on the server.'
101 | )
102 | )
103 | })
104 | ).toBeFalsy()
105 |
106 | // @ts-expect-error
107 | console.error.mockRestore()
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/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"],
23 | "exclude": ["./**/dist", "examples"],
24 | "watchOptions": {
25 | "watchFile": "useFsEvents",
26 | "watchDirectory": "useFsEvents",
27 | "fallbackPolling": "dynamicPriority"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------