├── .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 | 13 | {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 | 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 | 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 | 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 | 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 | 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 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/api-hooks) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/api-hooks 15 | cd api-hooks 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Show how you could create custom hooks, using SWR internally, for your different data requirements and use them in your application. 31 | -------------------------------------------------------------------------------- /examples/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 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/autocomplete-suggestions) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/autocomplete-suggestions 15 | cd autocomplete-suggestions 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Show how to use SWR to fetch the suggestion for an autocomplete. 31 | -------------------------------------------------------------------------------- /examples/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 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/axios-typescript) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/axios-typescript 15 | cd axios-typescript 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Show how to use the basic axios along with TypeScript to type both the request object and the data received from SWR. 31 | -------------------------------------------------------------------------------- /examples/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 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/axios) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/axios 15 | cd axios 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Show a basic usage of SWR fetching using axios and a request object. 31 | -------------------------------------------------------------------------------- /examples/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 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/basic-typescript) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/basic-typescript 15 | cd basic-typescript 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Show how to use the basic example along with TypeScript to type the data received from SWR. 31 | -------------------------------------------------------------------------------- /examples/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 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/basic) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/basic 15 | cd basic 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Show a basic usage of SWR fetching data from an API in two different pages. 31 | -------------------------------------------------------------------------------- /examples/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 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/focus-revalidate) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/focus-revalidate 15 | cd focus-revalidate 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Basic authentication example showing how the revalidate on focus feature works and to trigger a revalidation on a per-hook call basis. 31 | -------------------------------------------------------------------------------- /examples/focus-revalidate/components/button.js: -------------------------------------------------------------------------------- 1 | export default function Button({ children, ...props }) { 2 | return
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 | 19 |
20 | } else { 21 | return
22 |

Please login

23 | 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 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/global-fetcher) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/global-fetcher 15 | cd global-fetcher 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Use the `SWRConfig` provider to set up the fetcher globally instead of a per-hook call. 31 | -------------------------------------------------------------------------------- /examples/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 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/infinite-scroll) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/infinite-scroll 15 | cd infinite-scroll 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Show usage of useSWRInfinite with infinite scroll based on IntersectionObserver 31 | -------------------------------------------------------------------------------- /examples/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 | 59 |

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

69 | {isEmpty ?

Yay, no issues found.

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

73 | - {issue.title} 74 |

75 | ) 76 | })} 77 |
78 | {isLoadingMore ? 'loading...' : isReachingEnd ? 'no more issues' : ''} 79 |
80 |
81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /examples/infinite/README.md: -------------------------------------------------------------------------------- 1 | # useSWRInfinite 2 | 3 | ## One-Click Deploy 4 | 5 | Deploy your own SWR project with Vercel. 6 | 7 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/infinite) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/infinite 15 | cd basic 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Show usage of useSWRInfinite with load more data button 31 | -------------------------------------------------------------------------------- /examples/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 | 45 |

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

65 | {isEmpty ?

Yay, no issues found.

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

69 | - {issue.title} 70 |

71 | ) 72 | })} 73 |
74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /examples/local-state-sharing/README.md: -------------------------------------------------------------------------------- 1 | # Basic 2 | 3 | ## One-Click Deploy 4 | 5 | Deploy your own SWR project with Vercel. 6 | 7 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/local-state-sharing) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/local-state-sharing 15 | cd local-state-sharing 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Show how to share local state between React components using SWR. 31 | -------------------------------------------------------------------------------- /examples/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 | 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 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/optimistic-ui-immer) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/optimistic-ui-immer 15 | cd optimistic-ui-immer 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Example of how to use SWR and Immer to implement an Optimistic UI pattern where we mutate the cached data immediately and then trigger a revalidation with the API. 31 | -------------------------------------------------------------------------------- /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 |
35 | setText(event.target.value)} 38 | value={text} 39 | /> 40 | 41 |
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 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/optimistic-ui) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/optimistic-ui 15 | cd optimistic-ui 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Example of how to use SWR to implement an Optimistic UI pattern where we mutate the cached data immediately and then trigger a revalidation with the API. 31 | -------------------------------------------------------------------------------- /examples/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 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/prefetch-preload) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/prefetch-preload 15 | cd prefetch-preload 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | This example shows multiple ways to prefetch data to be used by SWR later. 31 | 32 | - Use a `` to get the browser to load the data while rendering the HTML 33 | - If in a browser, run the fetch + mutate outside the component 34 | - After rendering use an effect in React to prefetch the next page's data 35 | - When the user moves the mouse over a link trigger a fetch + mutate for the next page 36 | 37 | In the real world you would not necessarily use all of them at the same time but one or more combined to give the best UX possible. 38 | -------------------------------------------------------------------------------- /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 | [![Deploy with Vercel Now](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/refetch-interval) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/refetch-interval 15 | cd refetch-interval 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Show how to make SWR fetch the API again in an interval automatically to ensure the data is up-to-date. 31 | -------------------------------------------------------------------------------- /examples/refetch-interval/components/button.js: -------------------------------------------------------------------------------- 1 | export default function Button({ children, ...props }) { 2 | return
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 |
{ 21 | ev.preventDefault() 22 | setValue('') 23 | await fetch(`/api/data?add=${value}`) 24 | mutate() 25 | }}> 26 | setValue(ev.target.value)} /> 27 |
28 |
    29 | {data.map(item =>
  • {item}
  • )} 30 |
31 | 35 |
36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /examples/server-render/README.md: -------------------------------------------------------------------------------- 1 | # Server Render 2 | 3 | ## One-Click Deploy 4 | 5 | Deploy your own SWR project with Vercel. 6 | 7 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/server-render) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/server-render 15 | cd server-render 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | This example shows how to combine Next.js getServerSideProps with the SWR `fallbackData` option to support Server-Side Rendering. 31 | 32 | The application will fetch the data server-side and then receive it as props, that data will be passed as `fallbackData` to SWR, once the application starts client-side SWR will revalidate it against the API and update the DOM, if it's required, with the new data. 33 | -------------------------------------------------------------------------------- /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 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/storage-tab-sync) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/storage-tab-sync 15 | cd storage-tab-sync 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Show how you could use SWR to synchronize localStorage values between tabs. 31 | -------------------------------------------------------------------------------- /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 | 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 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/subscription) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/subscription 15 | cd subscription 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Show how you could use SWR to subscribe async observable data into your app. 31 | -------------------------------------------------------------------------------- /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 | 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 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/suspense) 8 | 9 | ## How to Use 10 | 11 | Download the example: 12 | 13 | ```bash 14 | curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/suspense 15 | cd suspense 16 | ``` 17 | 18 | Install it and run: 19 | 20 | ```bash 21 | yarn 22 | yarn dev 23 | # or 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | ## The Idea behind the Example 29 | 30 | Show how to use the SWR suspense option with React suspense. 31 | -------------------------------------------------------------------------------- /examples/suspense/app/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 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 | --------------------------------------------------------------------------------