├── .all-contributorsrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── CODEOWNERS └── workflows │ └── main.yml ├── .gitignore ├── .npmignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── babel.config.js ├── docs ├── .vitepress │ ├── cache │ │ └── deps │ │ │ ├── @vue_devtools-api.js │ │ │ ├── _metadata.json │ │ │ ├── package.json │ │ │ └── vue.js │ ├── config.ts │ └── theme │ │ ├── index.css │ │ └── index.ts ├── configuration.md ├── features.md ├── guide.md ├── index.md ├── public │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── logo_45.png │ ├── robots.txt │ └── safari-pinned-tab.svg └── use-swrv.md ├── examples ├── axios-typescript-nuxt │ ├── .gitignore │ ├── .prettierrc │ ├── README.md │ ├── nuxt.config.js │ ├── package.json │ ├── pages │ │ └── index.vue │ ├── plugins │ │ └── compositionApi.ts │ ├── tsconfig.json │ └── yarn.lock ├── basic │ ├── README.md │ ├── package.json │ ├── src │ │ ├── App.vue │ │ ├── Repos.vue │ │ └── main.js │ └── yarn.lock ├── pwa │ ├── README.md │ ├── babel.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── robots.txt │ ├── src │ │ ├── App.vue │ │ ├── assets │ │ │ └── logo.png │ │ ├── components │ │ │ ├── TodoItem.vue │ │ │ └── TodosList.vue │ │ ├── main.js │ │ ├── registerServiceWorker.js │ │ └── useTodos.js │ └── yarn.lock ├── ssr-nuxt │ ├── Post.vue │ ├── README.md │ ├── layouts │ │ └── default.vue │ ├── main.css │ ├── nuxt.config.js │ ├── package.json │ ├── pages │ │ ├── _id.vue │ │ └── index.vue │ ├── plugins │ │ └── vue-composition-api.js │ └── yarn.lock └── vite │ ├── App.vue │ ├── README.md │ ├── components │ └── HelloWorld.vue │ ├── index.html │ ├── package.json │ ├── vite.config.js │ └── yarn.lock ├── jest.config.js ├── logo.png ├── netlify.toml ├── package.json ├── src ├── cache │ ├── adapters │ │ └── localStorage.ts │ └── index.ts ├── index.ts ├── lib │ ├── hash.ts │ └── web-preset.ts ├── types.ts └── use-swrv.ts ├── tests ├── cache.spec.tsx ├── ssr.spec.ts ├── test-compat-all.sh ├── test-compat.sh ├── use-swrv.spec.tsx └── utils │ ├── jest-timeout.ts │ └── tick.ts ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "swrv", 3 | "projectOwner": "Kong", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": false, 11 | "commitConvention": "none", 12 | "contributors": [ 13 | { 14 | "login": "darrenjennings", 15 | "name": "Darren Jennings", 16 | "avatar_url": "https://avatars2.githubusercontent.com/u/5770711?v=4", 17 | "profile": "https://guuu.io/", 18 | "contributions": [ 19 | "code", 20 | "doc" 21 | ] 22 | }, 23 | { 24 | "login": "Atinux", 25 | "name": "Sébastien Chopin", 26 | "avatar_url": "https://avatars2.githubusercontent.com/u/904724?v=4", 27 | "profile": "https://atinux.com", 28 | "contributions": [ 29 | "code", 30 | "ideas" 31 | ] 32 | }, 33 | { 34 | "login": "chuca", 35 | "name": "Fernando Machuca", 36 | "avatar_url": "https://avatars0.githubusercontent.com/u/864496?v=4", 37 | "profile": "https://github.com/chuca", 38 | "contributions": [ 39 | "design" 40 | ] 41 | }, 42 | { 43 | "login": "zeit", 44 | "name": "ZEIT", 45 | "avatar_url": "https://avatars0.githubusercontent.com/u/14985020?v=4", 46 | "profile": "https://zeit.co", 47 | "contributions": [ 48 | "ideas" 49 | ] 50 | }, 51 | { 52 | "login": "adamdehaven", 53 | "name": "Adam DeHaven", 54 | "avatar_url": "https://avatars.githubusercontent.com/u/2229946?v=4", 55 | "profile": "https://www.adamdehaven.com", 56 | "contributions": [ 57 | "code", 58 | "doc", 59 | "maintenance" 60 | ] 61 | } 62 | ], 63 | "contributorsPerLine": 7 64 | } 65 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | esm -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | 'plugin:vue/essential', 8 | '@vue/standard', 9 | '@vue/typescript' 10 | ], 11 | rules: { 12 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 13 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 14 | 'promise/param-names': 'off', 15 | 'no-multiple-empty-lines': ['error', { max: 1, maxEOF: 1 }], 16 | 'no-trailing-spaces': 'error', 17 | 'eol-last': 'error' 18 | }, 19 | parserOptions: { 20 | parser: '@typescript-eslint/parser' 21 | }, 22 | overrides: [ 23 | { 24 | files: [ 25 | '**/__tests__/*.{j,t}s?(x)', 26 | '**/tests/unit/**/*.spec.{j,t}s?(x)' 27 | ], 28 | env: { 29 | jest: true 30 | } 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # CI settings 2 | /.github/ @adamdehaven 3 | 4 | # Deployment settings 5 | /netlify.toml @adamdehaven 6 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - next 8 | pull_request: 9 | branches: 10 | - master 11 | - next 12 | 13 | jobs: 14 | test: 15 | name: Build and Test 16 | runs-on: ubuntu-latest 17 | timeout-minutes: 20 18 | strategy: 19 | matrix: 20 | node-version: [16.x] 21 | steps: 22 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - name: install, lint 28 | run: | 29 | yarn install --frozen-lockfile 30 | yarn lint --no-fix 31 | - name: tsc 32 | run: | 33 | yarn types:check 34 | 35 | - name: test 36 | run: | 37 | yarn test:compat 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | 23 | /esm 24 | /coverage 25 | .nuxt 26 | /docs/.vitepress/cache 27 | .yarn 28 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | *.tgz 4 | .env 5 | .next 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing :monkey_face: 2 | 3 | Hello, and welcome! Whether you are looking for help, trying to report a bug, 4 | thinking about getting involved in the project or about to submit a patch, this 5 | document is for you! Its intent is to be both an entry point for newcomers to 6 | the community (with various technical backgrounds), and a guide/reference for 7 | contributors and maintainers. 8 | 9 | Consult the Table of Contents below, and jump to the desired section. 10 | 11 | - [Development](#development) 12 | - [Code Style](#code-style) 13 | - [Linting](#linting) 14 | - [Running Tests](#running-tests) 15 | - [Verifying the Build Output](#verifying-the-build-output) 16 | - [Deploying](#deploying) 17 | 18 | ## Development 19 | 20 | This project uses the [vue-cli](https://cli.vuejs.org/) for linting, testing, 21 | and building. See package.json for all currently used plugins. 22 | 23 | ### Code Style 24 | 25 | If it passes linting, YOLO. Improvements to ASCII art encouraged. 26 | 27 | ### Linting 28 | 29 | ```sh 30 | # Autofixes any linting issues 31 | yarn lint 32 | 33 | # Outputs linting issues that need to be fixed without fixing 34 | yarn lint --no-fix 35 | ``` 36 | 37 | ### Running Tests 38 | 39 | Swrv has a suite of unit tests that are meant to be as comprehensive as 40 | possible. They run in CI and are required to pass in order to merge. 41 | 42 | ```sh 43 | # Run all tests 44 | yarn test 45 | 46 | # Run all tests and watch file changes to trigger a rerun (also enters jest mode to filter tests) 47 | yarn test --watchAll 48 | 49 | # Run just a single test file 50 | yarn test use-swrv 51 | ``` 52 | 53 | Tests can get you most of the way there when developing a new feature. However, 54 | you will want to test it in a real app eventually. 55 | 56 | ### Verifying the Build Output 57 | 58 | This could be better experience. If you want to develop swrv and test that the 59 | esm/dist bundles are working correctly, you can run the build command and copy 60 | the bundle to your project. 61 | 62 | ```sh 63 | yarn build 64 | ``` 65 | 66 | Output inside `esm`, and `dist` will contain output from `tsc` build. Move this 67 | into your project and change import statements. Using `yarn link` is an exercise 68 | left to the reader. Contributions to this doc are welcome if you get it working! 69 | 70 | ## Deploying 71 | 72 | > Note: this is for maintainers of the repo, with access to publish to NPM 73 | 74 | After merging a PR, you will want to get it up on the registry for everyone to 75 | use. 76 | 77 | 1. bump the version according to [semver](https://semver.org/) in the 78 | package.json of the repo with the appropriate new version `x.x.x` 79 | 1. `git commit` with the message `chore(release) x.x.x` directly to your local 80 | master. 81 | 1. Build the library artifacts `yarn build` 82 | 1. Login as an authorized npm user (has access to swrv npm package) 83 | 1. `npm publish` 84 | 1. Once published, git push to origin/master 85 | 1. draft a github release following the naming conventions of the other 86 | releases. 87 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Kong Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

swrv

5 | 6 | [![](https://img.shields.io/npm/v/swrv.svg)](https://www.npmjs.com/package/swrv) [![npm](https://img.shields.io/npm/dm/swrv)](https://www.npmjs.com/package/swrv) ![build](https://github.com/Kong/swrv/workflows/build/badge.svg) 7 | 8 | `swrv` (pronounced "swerve") is a library using the [Vue Composition API](https://vuejs.org/guide/extras/composition-api-faq.html) for remote data fetching. It is largely a port of [swr](https://github.com/zeit/swr). 9 | 10 | - [Documentation](https://docs-swrv.netlify.app/) 11 | 12 | The name “SWR” is derived from stale-while-revalidate, a cache invalidation strategy popularized by HTTP [RFC 5861](https://tools.ietf.org/html/rfc5861). SWR first returns the data from cache (stale), then sends the fetch request (revalidate), and finally comes with the up-to-date data again. 13 | 14 | Features: 15 | 16 | - Transport and protocol agnostic data fetching 17 | - Fast page navigation 18 | - Interval polling 19 | - ~~SSR support~~ (removed as of version `0.10.0` - [read more](https://github.com/Kong/swrv/pull/304)) 20 | - Vue 3 Support 21 | - Revalidation on focus 22 | - Request deduplication 23 | - TypeScript ready 24 | - Minimal API 25 | - Stale-if-error 26 | - Customizable cache implementation 27 | - Error Retry 28 | 29 | With `swrv`, components will get a stream of data updates constantly and automatically. Thus, the UI will be always fast and reactive. 30 | 31 | ## Table of Contents 32 | 33 | - [Installation](#installation) 34 | - [Vue 3](#vue-3) 35 | - [Vue 2.7](#vue-27) 36 | - [Vue 2.6 and below](#vue-26-and-below) 37 | - [Getting Started](#getting-started) 38 | - [Api](#api) 39 | - [Parameters](#parameters) 40 | - [Return Values](#return-values) 41 | - [Config options](#config-options) 42 | - [Prefetching](#prefetching) 43 | - [Dependent Fetching](#dependent-fetching) 44 | - [Stale-if-error](#stale-if-error) 45 | - [State Management](#state-management) 46 | - [useSwrvState](#useswrvstate) 47 | - [Vuex](#vuex) 48 | - [Cache](#cache) 49 | - [localStorage](#localstorage) 50 | - [Serve from cache only](#serve-from-cache-only) 51 | - [Error Handling](#error-handling) 52 | - [FAQ](#faq) 53 | - [How is swrv different from the swr react library](#how-is-swrv-different-from-the-swr-react-library) 54 | - [Why does swrv make so many requests](#why-does-swrv-make-so-many-requests) 55 | - [How can I refetch swrv data to update it](#how-can-i-refetch-swrv-data-to-update-it) 56 | - [Contributors ✨](#contributors-) 57 | 58 | ## Installation 59 | 60 | The version of `swrv` you install depends on the Vue dependency in your project. 61 | 62 | ### Vue 3 63 | 64 | ```shell 65 | # Install the latest version 66 | yarn add swrv 67 | ``` 68 | 69 | ### Vue 2.7 70 | 71 | This version removes the dependency of the external `@vue/composition-api` plugin and adds `vue` to the `peerDependencies`, requiring a version that matches the following pattern: `>= 2.7.0 < 3` 72 | 73 | ```shell 74 | # Install the 0.10.x version for Vue 2.7 75 | yarn add swrv@v2-latest 76 | ``` 77 | 78 | ### Vue 2.6 and below 79 | 80 | If you're installing for Vue `2.6.x` and below, you may want to check out a [previous version of the README](https://github.com/Kong/swrv/blob/b621aac02b7780a4143c5743682070223e793b10/README.md) to view how to initialize `swrv` utilizing the external `@vue/composition-api` plugin. 81 | 82 | ```shell 83 | # Install the 0.9.x version for Vue < 2.7 84 | yarn add swrv@legacy 85 | ``` 86 | 87 | ## Getting Started 88 | 89 | ```vue 90 | 97 | 98 | 114 | ``` 115 | 116 | In this example, the Vue Hook `useSWRV` accepts a `key` and a `fetcher` function. `key` is a unique identifier of the request, normally the URL of the API. And the fetcher accepts key as its parameter and returns the data asynchronously. 117 | 118 | `useSWRV` also returns 2 values: `data` and `error`. When the request (fetcher) is not yet finished, data will be `undefined`. And when we get a response, it sets `data` and `error` based on the result of fetcher and rerenders the component. This is because `data` and `error` are Vue [Refs](https://vuejs.org/api/reactivity-core.html#ref), and their values will be set by the fetcher response. 119 | 120 | Note that fetcher can be any asynchronous function, so you can use your favorite data-fetching library to handle that part. When omitted, swrv falls back to the browser [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). 121 | 122 | ## Api 123 | 124 | ```ts 125 | const { data, error, isValidating, mutate } = useSWRV(key, fetcher, options) 126 | ``` 127 | 128 | ### Parameters 129 | 130 | | Param | Required | Description | 131 | | --------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 132 | | `key` | yes | a unique key string for the request (or a reactive reference / watcher function / null) (advanced usage) | 133 | | `fetcher` | | a Promise returning function to fetch your data. If `null`, swrv will fetch from cache only and not revalidate. If omitted (i.e. `undefined`) then the [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) api will be used. | 134 | | `options` | | an object of configuration options | 135 | 136 | ### Return Values 137 | 138 | - `data`: data for the given key resolved by fetcher (or undefined if not 139 | loaded) 140 | - `error`: error thrown by fetcher (or undefined) 141 | - `isValidating`: if there's a request or revalidation loading 142 | - `mutate`: function to trigger the validation manually 143 | 144 | ### Config options 145 | 146 | See [Config Defaults](https://github.com/Kong/swrv/blob/1587416e59dad12f9261e289b8cf63da81aa2dd4/src/use-swrv.ts#L43) 147 | 148 | - `refreshInterval = 0` - polling interval in milliseconds. 0 means this is 149 | disabled. 150 | - `dedupingInterval = 2000` - dedupe requests with the same key in this time 151 | span 152 | - `ttl = 0` - time to live of response data in cache. 0 mean it stays around 153 | forever. 154 | - `shouldRetryOnError = true` - retry when fetcher has an error 155 | - `errorRetryInterval = 5000` - error retry interval 156 | - `errorRetryCount: 5` - max error retry count 157 | - `revalidateOnFocus = true` - auto revalidate when window gets focused 158 | - `revalidateDebounce = 0` - debounce in milliseconds for revalidation. Useful 159 | for when a component is serving from the cache immediately, but then un-mounts 160 | soon thereafter (e.g. a user clicking "next" in pagination quickly) to avoid 161 | unnecessary fetches. 162 | - `cache` - caching instance to store response data in. See 163 | [src/lib/cache](src/lib/cache.ts), and [Cache](#cache) below. 164 | 165 | ## Prefetching 166 | 167 | Prefetching can be useful for when you anticipate user actions, like hovering over a link. SWRV exposes the `mutate` function so that results can be stored in the SWRV cache at a predetermined time. 168 | 169 | ```ts 170 | import { mutate } from 'swrv' 171 | 172 | function prefetch() { 173 | mutate( 174 | '/api/data', 175 | fetch('/api/data').then((res) => res.json()) 176 | ) 177 | // the second parameter is a Promise 178 | // SWRV will use the result when it resolves 179 | } 180 | ``` 181 | 182 | ## Dependent Fetching 183 | 184 | swrv also allows you to fetch data that depends on other data. It ensures the maximum possible parallelism (avoiding waterfalls), as well as serial fetching when a piece of dynamic data is required for the next data fetch to happen. 185 | 186 | ```vue 187 | 191 | 192 | 214 | ``` 215 | 216 | ## Stale-if-error 217 | 218 | One of the benefits of a stale content caching strategy is that the cache can be served when requests fail.`swrv` uses a [stale-if-error](https://tools.ietf.org/html/rfc5861#section-4) strategy and will maintain `data` in the cache even if a `useSWRV` fetch returns an `error`. 219 | 220 | ```vue 221 | 229 | 230 | 249 | ``` 250 | 251 | ## State Management 252 | 253 | ### useSwrvState 254 | 255 | Sometimes you might want to know the exact state where swrv is during stale-while-revalidate lifecyle. This is helpful when representing the UI as a function of state. Here is one way to detect state using a user-land composable `useSwrvState` function: 256 | 257 | ```js 258 | import { ref, watchEffect } from 'vue' 259 | 260 | const STATES = { 261 | VALIDATING: 'VALIDATING', 262 | PENDING: 'PENDING', 263 | SUCCESS: 'SUCCESS', 264 | ERROR: 'ERROR', 265 | STALE_IF_ERROR: 'STALE_IF_ERROR', 266 | } 267 | 268 | export default function(data, error, isValidating) { 269 | const state = ref('idle') 270 | watchEffect(() => { 271 | if (data.value && isValidating.value) { 272 | state.value = STATES.VALIDATING 273 | return 274 | } 275 | if (data.value && error.value) { 276 | state.value = STATES.STALE_IF_ERROR 277 | return 278 | } 279 | if (data.value === undefined && !error.value) { 280 | state.value = STATES.PENDING 281 | return 282 | } 283 | if (data.value && !error.value) { 284 | state.value = STATES.SUCCESS 285 | return 286 | } 287 | if (data.value === undefined && error) { 288 | state.value = STATES.ERROR 289 | return 290 | } 291 | }) 292 | 293 | return { 294 | state, 295 | STATES, 296 | } 297 | } 298 | ``` 299 | 300 | And then in your template you can use it like so: 301 | 302 | ```vue 303 | 323 | 324 | 350 | ``` 351 | 352 | ### Vuex 353 | 354 | Most of the features of swrv handle the complex logic / ceremony that you'd have to implement yourself inside a vuex store. All swrv instances use the same global cache, so if you are using swrv alongside vuex, you can use global watchers on resolved swrv returned refs. It is encouraged to wrap useSWRV in a custom composable function so that you can do application level side effects if desired (e.g. dispatch a vuex action when data changes to log events or perform some logic). 355 | 356 | Vue 3 example: 357 | 358 | ```vue 359 | 391 | ``` 392 | 393 | ## Cache 394 | 395 | By default, a custom cache implementation is used to store fetcher response data cache, in-flight promise cache, and ref cache. Response data cache can be customized via the `config.cache` property. Built in cache adapters: 396 | 397 | ### localStorage 398 | 399 | A common usage case to have a better _offline_ experience is to read from `localStorage`. Checkout the [PWA example](https://github.com/Kong/swrv/tree/master/examples/pwa) for more inspiration. 400 | 401 | ```ts 402 | import useSWRV from 'swrv' 403 | import LocalStorageCache from 'swrv/dist/cache/adapters/localStorage' 404 | 405 | function useTodos () { 406 | const { data, error } = useSWRV('/todos', undefined, { 407 | cache: new LocalStorageCache('swrv'), 408 | shouldRetryOnError: false 409 | }) 410 | 411 | return { 412 | data, 413 | error 414 | } 415 | } 416 | ``` 417 | 418 | ### Serve from cache only 419 | 420 | To only retrieve a swrv cache response without revalidating, you can set the fetcher function to `null` from the useSWRV call. This can be useful when there is some higher level swrv composable that is always sending data to other instances, so you can assume that composables with a `null` fetcher will have data available. This [isn't very intuitive](https://github.com/Kong/swrv/issues/148), so will be looking for ways to improve this api in the future. 421 | 422 | ```ts 423 | // Component A 424 | const { data } = useSWRV('/api/config', fetcher) 425 | 426 | // Component B, only retrieve from cache 427 | const { data } = useSWRV('/api/config', null) 428 | ``` 429 | 430 | ## Error Handling 431 | 432 | Since `error` is returned as a Vue Ref, you can use watchers to handle any onError callback functionality. Check out [the test](https://github.com/Kong/swrv/blob/a063c4aa142a5a13dbd39496cefab7aef54e610c/tests/use-swrv.spec.tsx#L481). 433 | 434 | ```ts 435 | export default { 436 | setup() { 437 | const { data, error } = useSWRV(key, fetch) 438 | 439 | function handleError(error) { 440 | console.error(error && error.message) 441 | } 442 | 443 | watch(error, handleError) 444 | 445 | return { 446 | data, 447 | error, 448 | } 449 | }, 450 | } 451 | ``` 452 | 453 | ## FAQ 454 | 455 | ### How is swrv different from the [swr](https://github.com/zeit/swr) react library 456 | 457 | #### Vue and Reactivity 458 | 459 | The `swrv` library is meant to be used with the Vue Composition API (and eventually Vue 3) so it utilizes Vue's reactivity system to track dependencies and returns vue `Ref`'s as it's return values. This allows you to watch `data` or build your own computed props. For example, the key function is implemented as Vue `watch`er, so any changes to the dependencies in this function will trigger a revalidation in `swrv`. 460 | 461 | #### Features 462 | 463 | Features were built as needed for `swrv`, and while the initial development of `swrv` was mostly a port of swr, the feature sets are not 1-1, and are subject to diverge as they already have. 464 | 465 | ### Why does swrv make so many requests 466 | 467 | The idea behind stale-while-revalidate is that you always get fresh data eventually. You can disable some of the eager fetching such as `config.revalidateOnFocus`, but it is preferred to serve a fast response from cache while also revalidating so users are always getting the most up to date data. 468 | 469 | ### How can I refetch swrv data to update it 470 | 471 | Swrv fetcher functions can be triggered on-demand by using the `mutate` [return value](https://github.com/Kong/swrv/#return-values). This is useful when there is some event that needs to trigger a revalidation such a PATCH request that updates the initial GET request response data. 472 | 473 | ## Contributors ✨ 474 | 475 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 |

Darren Jennings

💻 📖

Sébastien Chopin

💻 🤔

Fernando Machuca

🎨

ZEIT

🤔

Adam DeHaven

💻 📖 🚧
489 | 490 | 491 | 492 | 493 | 494 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 495 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/@vue_devtools-api.js: -------------------------------------------------------------------------------- 1 | // node_modules/@vue/devtools-api/lib/esm/env.js 2 | function getDevtoolsGlobalHook() { 3 | return getTarget().__VUE_DEVTOOLS_GLOBAL_HOOK__; 4 | } 5 | function getTarget() { 6 | return typeof navigator !== "undefined" && typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : {}; 7 | } 8 | var isProxyAvailable = typeof Proxy === "function"; 9 | 10 | // node_modules/@vue/devtools-api/lib/esm/const.js 11 | var HOOK_SETUP = "devtools-plugin:setup"; 12 | var HOOK_PLUGIN_SETTINGS_SET = "plugin:settings:set"; 13 | 14 | // node_modules/@vue/devtools-api/lib/esm/time.js 15 | var supported; 16 | var perf; 17 | function isPerformanceSupported() { 18 | var _a; 19 | if (supported !== void 0) { 20 | return supported; 21 | } 22 | if (typeof window !== "undefined" && window.performance) { 23 | supported = true; 24 | perf = window.performance; 25 | } else if (typeof global !== "undefined" && ((_a = global.perf_hooks) === null || _a === void 0 ? void 0 : _a.performance)) { 26 | supported = true; 27 | perf = global.perf_hooks.performance; 28 | } else { 29 | supported = false; 30 | } 31 | return supported; 32 | } 33 | function now() { 34 | return isPerformanceSupported() ? perf.now() : Date.now(); 35 | } 36 | 37 | // node_modules/@vue/devtools-api/lib/esm/proxy.js 38 | var ApiProxy = class { 39 | constructor(plugin, hook) { 40 | this.target = null; 41 | this.targetQueue = []; 42 | this.onQueue = []; 43 | this.plugin = plugin; 44 | this.hook = hook; 45 | const defaultSettings = {}; 46 | if (plugin.settings) { 47 | for (const id in plugin.settings) { 48 | const item = plugin.settings[id]; 49 | defaultSettings[id] = item.defaultValue; 50 | } 51 | } 52 | const localSettingsSaveId = `__vue-devtools-plugin-settings__${plugin.id}`; 53 | let currentSettings = Object.assign({}, defaultSettings); 54 | try { 55 | const raw = localStorage.getItem(localSettingsSaveId); 56 | const data = JSON.parse(raw); 57 | Object.assign(currentSettings, data); 58 | } catch (e) { 59 | } 60 | this.fallbacks = { 61 | getSettings() { 62 | return currentSettings; 63 | }, 64 | setSettings(value) { 65 | try { 66 | localStorage.setItem(localSettingsSaveId, JSON.stringify(value)); 67 | } catch (e) { 68 | } 69 | currentSettings = value; 70 | }, 71 | now() { 72 | return now(); 73 | } 74 | }; 75 | if (hook) { 76 | hook.on(HOOK_PLUGIN_SETTINGS_SET, (pluginId, value) => { 77 | if (pluginId === this.plugin.id) { 78 | this.fallbacks.setSettings(value); 79 | } 80 | }); 81 | } 82 | this.proxiedOn = new Proxy({}, { 83 | get: (_target, prop) => { 84 | if (this.target) { 85 | return this.target.on[prop]; 86 | } else { 87 | return (...args) => { 88 | this.onQueue.push({ 89 | method: prop, 90 | args 91 | }); 92 | }; 93 | } 94 | } 95 | }); 96 | this.proxiedTarget = new Proxy({}, { 97 | get: (_target, prop) => { 98 | if (this.target) { 99 | return this.target[prop]; 100 | } else if (prop === "on") { 101 | return this.proxiedOn; 102 | } else if (Object.keys(this.fallbacks).includes(prop)) { 103 | return (...args) => { 104 | this.targetQueue.push({ 105 | method: prop, 106 | args, 107 | resolve: () => { 108 | } 109 | }); 110 | return this.fallbacks[prop](...args); 111 | }; 112 | } else { 113 | return (...args) => { 114 | return new Promise((resolve) => { 115 | this.targetQueue.push({ 116 | method: prop, 117 | args, 118 | resolve 119 | }); 120 | }); 121 | }; 122 | } 123 | } 124 | }); 125 | } 126 | async setRealTarget(target) { 127 | this.target = target; 128 | for (const item of this.onQueue) { 129 | this.target.on[item.method](...item.args); 130 | } 131 | for (const item of this.targetQueue) { 132 | item.resolve(await this.target[item.method](...item.args)); 133 | } 134 | } 135 | }; 136 | 137 | // node_modules/@vue/devtools-api/lib/esm/index.js 138 | function setupDevtoolsPlugin(pluginDescriptor, setupFn) { 139 | const descriptor = pluginDescriptor; 140 | const target = getTarget(); 141 | const hook = getDevtoolsGlobalHook(); 142 | const enableProxy = isProxyAvailable && descriptor.enableEarlyProxy; 143 | if (hook && (target.__VUE_DEVTOOLS_PLUGIN_API_AVAILABLE__ || !enableProxy)) { 144 | hook.emit(HOOK_SETUP, pluginDescriptor, setupFn); 145 | } else { 146 | const proxy = enableProxy ? new ApiProxy(descriptor, hook) : null; 147 | const list = target.__VUE_DEVTOOLS_PLUGINS__ = target.__VUE_DEVTOOLS_PLUGINS__ || []; 148 | list.push({ 149 | pluginDescriptor: descriptor, 150 | setupFn, 151 | proxy 152 | }); 153 | if (proxy) 154 | setupFn(proxy.proxiedTarget); 155 | } 156 | } 157 | export { 158 | isPerformanceSupported, 159 | now, 160 | setupDevtoolsPlugin 161 | }; 162 | //# sourceMappingURL=@vue_devtools-api.js.map 163 | -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/_metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "hash": "64996499", 3 | "browserHash": "a7e55216", 4 | "optimized": { 5 | "vue": { 6 | "src": "../../../../node_modules/vue/dist/vue.runtime.esm-bundler.js", 7 | "file": "vue.js", 8 | "fileHash": "b250038a", 9 | "needsInterop": false 10 | }, 11 | "@vue/devtools-api": { 12 | "src": "../../../../node_modules/@vue/devtools-api/lib/esm/index.js", 13 | "file": "@vue_devtools-api.js", 14 | "fileHash": "fe26a06a", 15 | "needsInterop": false 16 | } 17 | }, 18 | "chunks": {} 19 | } -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/package.json: -------------------------------------------------------------------------------- 1 | {"type":"module"} -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | 3 | export default defineConfig({ 4 | title: 'swrv', 5 | description: 'swrv', 6 | head: [ 7 | ['link', { rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png' }], 8 | ['link', { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-32x32.png' }], 9 | ['link', { rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicon-16x16.png' }], 10 | ['link', { rel: 'mask-icon', href: '/safari-pinned-tab.svg', color: '#5bbad5' }], 11 | ], 12 | lastUpdated: true, 13 | themeConfig: { 14 | outline: [2, 3], 15 | socialLinks: [ 16 | { icon: 'github', link: 'https://github.com/Kong/swrv'} 17 | ], 18 | logo: '/logo_45.png', 19 | footer: { 20 | message: 'Released under the Apache-2.0 License.', 21 | copyright: 'Copyright © 2020-present Kong, Inc.' 22 | }, 23 | editLink: { 24 | pattern: 'https://github.com/Kong/swrv/edit/master/docs/:path', 25 | text: 'Edit this page on GitHub' 26 | }, 27 | nav: [ 28 | { text: 'Home', link: '/' }, 29 | { text: 'Guide', link: '/guide' }, 30 | { text: 'Features', link: '/features' }, 31 | { text: 'API Reference', link: '/use-swrv' } 32 | ], 33 | sidebar: [ 34 | { 35 | text: 'Guide', 36 | items: [ 37 | { 38 | text: 'Getting Started', 39 | link: '/guide' 40 | }, 41 | { 42 | text: 'Features', 43 | link: '/features' 44 | } 45 | ] 46 | }, 47 | { 48 | text: 'APIs', 49 | items: [ 50 | { 51 | text: 'useSWRV', 52 | link: '/use-swrv' 53 | } 54 | ] 55 | } 56 | ], 57 | algolia: { 58 | appId: 'PN54XPFSKF', 59 | apiKey: '4dc7f3773a76d6375d2a286f647d02dc', 60 | indexName: 'swrv' 61 | }, 62 | }, 63 | }) 64 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --c-brand: #0089eb; 3 | --c-brand-light: #0798ff; 4 | overflow-y: scroll; 5 | } 6 | 7 | .nav-bar .logo { 8 | height: 25px; 9 | margin-right: 0.5rem; 10 | } 11 | 12 | .custom-block.tip { 13 | border-color: var(--c-brand-light); 14 | } 15 | 16 | .vp-doc div[class*='language-'] { 17 | color-scheme: dark; 18 | } 19 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import DefaultTheme from 'vitepress/theme' 2 | import './index.css' 3 | 4 | export default { 5 | ...DefaultTheme, 6 | } 7 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Page Moved 3 | --- 4 | 5 | # {{ $frontmatter.title }} 6 | 7 | The page you are looking for has been moved to [`/use-swrv`](/use-swrv). 8 | 9 | Automatically redirecting in {{ seconds }} seconds... 10 | 11 | 35 | -------------------------------------------------------------------------------- /docs/features.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Features 3 | --- 4 | 5 | # {{ $frontmatter.title }} 6 | 7 | ## Prefetching 8 | 9 | Prefetching can be useful for when you anticipate user actions, like hovering over a link. SWRV exposes the `mutate` function so that results can be stored in the SWRV cache at a predetermined time. 10 | 11 | ```ts 12 | import { mutate } from 'swrv' 13 | 14 | function prefetch() { 15 | mutate( 16 | '/api/data', 17 | fetch('/api/data').then((res) => res.json()) 18 | ) 19 | // the second parameter is a Promise 20 | // SWRV will use the result when it resolves 21 | } 22 | ``` 23 | 24 | ## Dependent Fetching 25 | 26 | swrv also allows you to fetch data that depends on other data. It ensures the maximum possible parallelism (avoiding waterfalls), as well as serial fetching when a piece of dynamic data is required for the next data fetch to happen. 27 | 28 | ```vue 29 | 33 | 34 | 56 | ``` 57 | 58 | ## Stale-if-error 59 | 60 | One of the benefits of a stale content caching strategy is that the cache can be served when requests fail.`swrv` uses a [stale-if-error](https://tools.ietf.org/html/rfc5861#section-4) strategy and will maintain `data` in the cache even if a `useSWRV` fetch returns an `error`. 61 | 62 | ```vue 63 | 71 | 72 | 91 | ``` 92 | 93 | ## State Management 94 | 95 | ### useSwrvState 96 | 97 | Sometimes you might want to know the exact state where swrv is during stale-while-revalidate lifecyle. This is helpful when representing the UI as a function of state. Here is one way to detect state using a user-land composable `useSwrvState` function: 98 | 99 | ```js 100 | import { ref, watchEffect } from 'vue' 101 | 102 | const STATES = { 103 | VALIDATING: 'VALIDATING', 104 | PENDING: 'PENDING', 105 | SUCCESS: 'SUCCESS', 106 | ERROR: 'ERROR', 107 | STALE_IF_ERROR: 'STALE_IF_ERROR', 108 | } 109 | 110 | export default function(data, error, isValidating) { 111 | const state = ref('idle') 112 | watchEffect(() => { 113 | if (data.value && isValidating.value) { 114 | state.value = STATES.VALIDATING 115 | return 116 | } 117 | if (data.value && error.value) { 118 | state.value = STATES.STALE_IF_ERROR 119 | return 120 | } 121 | if (data.value === undefined && !error.value) { 122 | state.value = STATES.PENDING 123 | return 124 | } 125 | if (data.value && !error.value) { 126 | state.value = STATES.SUCCESS 127 | return 128 | } 129 | if (data.value === undefined && error) { 130 | state.value = STATES.ERROR 131 | return 132 | } 133 | }) 134 | 135 | return { 136 | state, 137 | STATES, 138 | } 139 | } 140 | ``` 141 | 142 | And then in your template you can use it like so: 143 | 144 | ```vue 145 | 159 | 160 | 186 | ``` 187 | 188 | ### Vuex 189 | 190 | Most of the features of swrv handle the complex logic / ceremony that you'd have to implement yourself inside a vuex store. All swrv instances use the same global cache, so if you are using swrv alongside vuex, you can use global watchers on resolved swrv returned refs. It is encouraged to wrap useSWRV in a custom composable function so that you can do application level side effects if desired (e.g. dispatch a vuex action when data changes to log events or perform some logic). 191 | 192 | Vue 3 example 193 | 194 | ```vue 195 | 227 | ``` 228 | 229 | ## Error Handling 230 | 231 | Since `error` is returned as a Vue Ref, you can use watchers to handle any onError callback functionality. Check out [the test](https://github.com/Kong/swrv/blob/a063c4aa142a5a13dbd39496cefab7aef54e610c/tests/use-swrv.spec.tsx#L481). 232 | 233 | ```ts 234 | export default { 235 | setup() { 236 | const { data, error } = useSWRV(key, fetch) 237 | 238 | function handleError(error) { 239 | console.error(error && error.message) 240 | } 241 | 242 | watch(error, handleError) 243 | 244 | return { 245 | data, 246 | error, 247 | } 248 | }, 249 | } 250 | ``` 251 | 252 | ## Cache 253 | 254 | By default, a custom cache implementation is used to store fetcher response data cache, in-flight promise cache, and ref cache. Response data cache can be customized via the `config.cache` property. Built in cache adapters: 255 | 256 | ### localStorage 257 | 258 | A common usage case to have a better _offline_ experience is to read from `localStorage`. Checkout the [PWA example](https://github.com/Kong/swrv/tree/master/examples/pwa) for more inspiration. 259 | 260 | ```ts 261 | import useSWRV from 'swrv' 262 | import LocalStorageCache from 'swrv/dist/cache/adapters/localStorage' 263 | 264 | function useTodos () { 265 | const { data, error } = useSWRV('/todos', undefined, { 266 | cache: new LocalStorageCache('swrv'), 267 | shouldRetryOnError: false 268 | }) 269 | 270 | return { 271 | data, 272 | error 273 | } 274 | } 275 | ``` 276 | 277 | ### Serve from cache only 278 | 279 | To only retrieve a swrv cache response without revalidating, you can set the fetcher function to `null` from the useSWRV call. This can be useful when there is some higher level swrv composable that is always sending data to other instances, so you can assume that composables with a `null` fetcher will have data available. This [isn't very intuitive](https://github.com/Kong/swrv/issues/148), so will be looking for ways to improve this api in the future. 280 | 281 | ```ts 282 | // Component A 283 | const { data } = useSWRV('/api/config', fetcher) 284 | 285 | // Component B, only retrieve from cache 286 | const { data } = useSWRV('/api/config', null) 287 | ``` 288 | -------------------------------------------------------------------------------- /docs/guide.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | --- 4 | 5 | # {{ $frontmatter.title }} 6 | 7 | | Version | Downloads | Build | 8 | | --------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 9 | | [![](https://img.shields.io/npm/v/swrv.svg)](https://www.npmjs.com/package/swrv) | [![npm](https://img.shields.io/npm/dm/swrv)](https://www.npmjs.com/package/swrv) | [![build](https://github.com/Kong/swrv/workflows/build/badge.svg)](https://github.com/Kong/swrv) | 10 | 11 | ## Overview 12 | 13 | `swrv` (pronounced "swerve") is a library using [Vue Composition API](https://vuejs.org/guide/extras/composition-api-faq.html) hooks for remote data fetching. It is largely a port of [swr](https://github.com/zeit/swr). 14 | 15 | The name “SWR” is derived from stale-while-revalidate, a cache invalidation strategy popularized by HTTP [RFC 5861](https://tools.ietf.org/html/rfc5861). SWR first returns the data from cache (stale), then sends the fetch request (revalidate), and finally comes with the up-to-date data again. 16 | 17 | ## Features 18 | 19 | - 📡 Transport and protocol agnostic data fetching 20 | - ⚡️ Fast page navigation 21 | - ⏲ Interval polling 22 | - ~~🖥 SSR support~~ (removed as of version `0.10.0` - [read more](https://github.com/Kong/swrv/pull/304)) 23 | - 🖖 Vue 3 Support 24 | - 🖖 Vue 2.7.x Support (under the `v2-latest` tag on npm) 25 | - 🖖 Vue <= 2.6.x Support (under the `legacy` tag on npm) 26 | - Revalidation on focus 27 | - Request deduplication 28 | - TypeScript ready 29 | - Minimal API 30 | - Stale-if-error 31 | - Customizable cache implementation 32 | - Error Retry 33 | 34 | With `swrv`, components will get a stream of data updates constantly and automatically. Thus, the UI will be always fast and reactive. 35 | 36 | ## Installation 37 | 38 | The version of `swrv` you install depends on the Vue dependency in your project. 39 | 40 | ### Vue 3 41 | 42 | ```shell 43 | # Install the latest version 44 | yarn add swrv 45 | ``` 46 | 47 | ### Vue 2.7 48 | 49 | This version removes the dependency of the external `@vue/composition-api` plugin and adds `vue` to the `peerDependencies`, requiring a version that matches the following pattern: `>= 2.7.0 < 3` 50 | 51 | ```shell 52 | # Install the 0.10.x version for Vue 2.7 53 | yarn add swrv@v2-latest 54 | ``` 55 | 56 | ### Vue 2.6 and below 57 | 58 | If you're installing for Vue `2.6.x` and below, you may want to check out a [previous version of the README](https://github.com/Kong/swrv/blob/b621aac02b7780a4143c5743682070223e793b10/README.md) to view how to initialize `swrv` utilizing the external `@vue/composition-api` plugin. 59 | 60 | ```shell 61 | # Install the 0.9.x version for Vue < 2.7 62 | yarn add swrv@legacy 63 | ``` 64 | 65 | ## Usage 66 | 67 | ```vue 68 | 75 | 76 | 90 | ``` 91 | 92 | In this example, the Vue Hook `useSWRV` accepts a `key` and a `fetcher` function. `key` is a unique identifier of the request, normally the URL of the API. And the fetcher accepts key as its parameter and returns the data asynchronously. 93 | 94 | `useSWRV` also returns 2 values: `data` and `error`. When the request (fetcher) is not yet finished, data will be `undefined`. And when we get a response, it sets `data` and `error` based on the result of fetcher and rerenders the component. This is because `data` and `error` are Vue [Refs](https://vuejs.org/api/reactivity-core.html#ref), and their values will be set by the fetcher response. 95 | 96 | Note that fetcher can be any asynchronous function, so you can use your favorite data-fetching library to handle that part. When omitted, swrv falls back to the browser [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). 97 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | hero: 5 | name: swrv 6 | text: A Vue library for data fetching 7 | tagline: With swrv, components will get a stream of data updates constantly and automatically. The UI will always be fast and reactive. 8 | image: 9 | src: /logo_45.png 10 | alt: swrv logo 11 | actions: 12 | - theme: brand 13 | text: Get Started 14 | link: /guide 15 | - theme: alt 16 | text: View on GitHub 17 | link: https://github.com/Kong/swrv 18 | 19 | features: 20 | - title: Feature-rich Data Fetching 21 | details: Transport and protocol agnostic data fetching, revalidation on focus, 22 | polling, in-flight de-duplication. 23 | - title: Vue Composition API 24 | details: Start developing with power of Vue 3, using the reactivity system of the Vue Composition API. 25 | - title: Stale-while-revalidate 26 | details: Uses cache to serve pages fast, while revalidating data sources producing an eventually consistent UI. 27 | --- 28 | -------------------------------------------------------------------------------- /docs/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kong/swrv/790a22cff70218c69020b1ef434e01cd3ec3ac5c/docs/public/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kong/swrv/790a22cff70218c69020b1ef434e01cd3ec3ac5c/docs/public/favicon-16x16.png -------------------------------------------------------------------------------- /docs/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kong/swrv/790a22cff70218c69020b1ef434e01cd3ec3ac5c/docs/public/favicon-32x32.png -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kong/swrv/790a22cff70218c69020b1ef434e01cd3ec3ac5c/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/logo_45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kong/swrv/790a22cff70218c69020b1ef434e01cd3ec3ac5c/docs/public/logo_45.png -------------------------------------------------------------------------------- /docs/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /configuration.html 3 | -------------------------------------------------------------------------------- /docs/public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /docs/use-swrv.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useSWRV 3 | --- 4 | 5 | # {{ $frontmatter.title }} {#useSWRV} 6 | 7 | ```ts 8 | const { 9 | data, error, isValidating, isLoading, mutate 10 | } = useSWRV(key, fetcher, options) 11 | ``` 12 | 13 | ## Parameters 14 | 15 | ### `key` 16 | 17 | - **Type**: `IKey` 18 | - **Required**: true 19 | 20 | ```ts 21 | type IKey = 22 | | string 23 | | any[] 24 | | null 25 | | undefined 26 | | WatchSource 27 | ``` 28 | 29 | A unique identifier for the request. This can be: 30 | 31 | - A string 32 | - An array (e.g., `[query, page]`), which gets hashed internally to produce a unique key 33 | - `null` or `undefined`, which disables fetching 34 | - A reactive reference or getter function returning one of the above types (string, array, `null`, or `undefined`) 35 | 36 | ### `fetcher` 37 | 38 | - **Type**: `(...args: any) => Data | Promise` 39 | 40 | A `Promise` returning function to fetch your data. If `null`, swrv will fetch from cache only and not revalidate. If omitted (i.e. `undefined`) then the fetch api will be used. 41 | 42 | If the resolved `key` value is an array, the fetcher function will be called with each element of the array as an argument. Otherwise, the fetcher function will be called with the resolved `key` value as the first argument. 43 | 44 | ### `options` 45 | 46 | - **Type**: `IConfig` 47 | 48 | An object of configuration options. See [Config options](#config-options). 49 | 50 | ## Return Values 51 | 52 | ### `data` 53 | 54 | - **Type**: `Ref` 55 | 56 | Data for the given key resolved by fetcher (or `undefined` if not loaded). 57 | 58 | ### `error` 59 | 60 | - **Type**: `Ref` 61 | 62 | Error thrown by fetcher (or `undefined`). 63 | 64 | ### `isValidating` 65 | 66 | - **Type**: `Ref` 67 | 68 | Becomes `true` whenever there is an ongoing request **whether the data is loaded or not**. 69 | 70 | ### `isLoading` 71 | 72 | - **Type**: `Ref` 73 | 74 | Becomes `true` when there is an ongoing request and **data is not loaded yet**. 75 | 76 | ### `mutate` 77 | 78 | - **Type**: `(data?: Data, options?: RevalidateOptions) => void` 79 | 80 | Function to trigger the validation manually. If `data` is provided, it will update the cache with the provided data. 81 | 82 | ```ts 83 | type Data = 84 | | (() => Promise | any) 85 | | Promise 86 | | any 87 | 88 | interface RevalidateOptions { 89 | shouldRetryOnError?: boolean, 90 | errorRetryCount?: number 91 | } 92 | ``` 93 | 94 | ## Config options 95 | 96 | See [Config Defaults](https://github.com/Kong/swrv/blob/1587416e59dad12f9261e289b8cf63da81aa2dd4/src/use-swrv.ts#L43) 97 | 98 | ### `refreshInterval` 99 | 100 | - **Type**: `number` 101 | - **Default**: `0` 102 | 103 | Polling interval in milliseconds. `0` means this is disabled. 104 | 105 | ### `dedupingInterval` 106 | 107 | - **Type**: `number` 108 | - **Default**: `2000` 109 | 110 | Dedupe requests with the same key in this time span. 111 | 112 | ### `ttl` 113 | 114 | - **Type**: `number` 115 | - **Default**: `0` 116 | 117 | Time to live of response data in cache. `0` means it stays around forever. 118 | 119 | ### `shouldRetryOnError` 120 | 121 | - **Type**: `boolean` 122 | - **Default**: `true` 123 | 124 | Retry when fetcher has an error. 125 | 126 | ### `errorRetryInterval` 127 | 128 | - **Type**: `number` 129 | - **Default**: `5000` 130 | 131 | Error retry interval. 132 | 133 | ### `errorRetryCount` 134 | 135 | - **Type**: `number` 136 | - **Default**: `5` 137 | 138 | Max error retry count. 139 | 140 | ### `revalidateOnFocus` 141 | 142 | - **Type**: `boolean` 143 | - **Default**: `true` 144 | 145 | Auto-revalidate when window gets focused. 146 | 147 | ### `revalidateDebounce` 148 | 149 | - **Type**: `number` 150 | - **Default**: `0` 151 | 152 | Debounce in milliseconds for revalidation. 153 | 154 | Useful for when a component is serving from the cache immediately, but then un-mounts soon thereafter (e.g. a user clicking "next" in pagination quickly) to avoid unnecessary fetches. 155 | 156 | ### `cache` 157 | 158 | Caching instance to store response data in. See [src/lib/cache](src/lib/cache.ts), and the [Cache](/features#cache) section. 159 | -------------------------------------------------------------------------------- /examples/axios-typescript-nuxt/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | /logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # Nuxt generate 72 | dist 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless 79 | 80 | # IDE / Editor 81 | .idea 82 | 83 | # Service worker 84 | sw.* 85 | 86 | # macOS 87 | .DS_Store 88 | 89 | # Vim swap files 90 | *.swp 91 | -------------------------------------------------------------------------------- /examples/axios-typescript-nuxt/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /examples/axios-typescript-nuxt/README.md: -------------------------------------------------------------------------------- 1 | # Axios TypeScript Nuxt Example 2 | 3 | > Note: This example is no longer functional now that SSR support is broken as of the Vue 2.7 upgrade 4 | -------------------------------------------------------------------------------- /examples/axios-typescript-nuxt/nuxt.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | mode: 'spa', 3 | plugins: [ 4 | './plugins/compositionApi.ts' 5 | ], 6 | buildModules: [ 7 | '@nuxt/typescript-build' 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /examples/axios-typescript-nuxt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swrv-axios-typescript-nuxt-example", 3 | "version": "1.0.0", 4 | "description": "My neat Nuxt.js project", 5 | "author": " ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "nuxt", 9 | "build": "nuxt build", 10 | "start": "nuxt start", 11 | "generate": "nuxt generate" 12 | }, 13 | "dependencies": { 14 | "axios": "^0.21.1", 15 | "nuxt": "^2.14.7" 16 | }, 17 | "devDependencies": { 18 | "@nuxt/typescript-build": "^0.6.0", 19 | "eslint-config-prettier": "^6.10.0", 20 | "eslint-plugin-prettier": "^3.1.2", 21 | "prettier": "^1.19.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/axios-typescript-nuxt/pages/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 93 | -------------------------------------------------------------------------------- /examples/axios-typescript-nuxt/plugins/compositionApi.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueCompositionApi from '../../../node_modules/@vue/composition-api' 3 | 4 | Vue.use(VueCompositionApi) 5 | -------------------------------------------------------------------------------- /examples/axios-typescript-nuxt/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "lib": [ 7 | "esnext", 8 | "esnext.asynciterable", 9 | "dom" 10 | ], 11 | "esModuleInterop": true, 12 | "allowJs": true, 13 | "sourceMap": true, 14 | "strict": true, 15 | "noEmit": true, 16 | "experimentalDecorators": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "~/*": [ 20 | "./*" 21 | ], 22 | "@/*": [ 23 | "./*" 24 | ] 25 | }, 26 | "types": [ 27 | "@types/node", 28 | "@nuxt/types" 29 | ] 30 | }, 31 | "exclude": [ 32 | "node_modules", 33 | ".nuxt", 34 | "dist" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 | # Basic Example 2 | 3 | Install dependencies 4 | 5 | ```sh 6 | yarn install 7 | ``` 8 | 9 | Run app: 10 | 11 | ```sh 12 | yarn serve 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swrv-basic-example", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "serve": "npx vue-cli-service serve" 6 | }, 7 | "dependencies": { 8 | "swrv": "latest" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/basic/src/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | 23 | 28 | -------------------------------------------------------------------------------- /examples/basic/src/Repos.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 43 | -------------------------------------------------------------------------------- /examples/basic/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App' 3 | 4 | createApp(App).mount('#app') 5 | -------------------------------------------------------------------------------- /examples/basic/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | swrv@^1.0.0-beta.8: 6 | version "1.0.0-beta.8" 7 | resolved "https://registry.yarnpkg.com/swrv/-/swrv-1.0.0-beta.8.tgz#745723f5ca8a7a7e290e911cf7da34cecaf356d3" 8 | integrity sha512-MsjaMOvZODfM0cess/HhbSrNbAotYinv4vzipLckKYBo/QmrvjNUPGZSRSqByXy/9AjrMRFWo0YanaVPbqADPQ== 9 | 10 | swrv@latest: 11 | version "0.9.6" 12 | resolved "https://registry.yarnpkg.com/swrv/-/swrv-0.9.6.tgz#27fd005577d40b5b9b827a8d75c316e9510f177b" 13 | integrity sha512-5C5f4BhzLzfDWt/XM9XS1W3yBuv5wbrrrgynkF7oXSugw2fk6K73jKpnUVYN3e/fmKccAt52Pab02OdQZjUxSA== 14 | dependencies: 15 | swrv "^1.0.0-beta.8" 16 | -------------------------------------------------------------------------------- /examples/pwa/README.md: -------------------------------------------------------------------------------- 1 | # PWA Example 2 | 3 | Install dependencies 4 | 5 | ```sh 6 | yarn install 7 | ``` 8 | 9 | Service worker: 10 | 11 | ```sh 12 | yarn build 13 | cd dist # service static content from here 14 | npx serve --cors -p 8007 15 | ``` 16 | 17 | Run app: 18 | 19 | ```sh 20 | yarn serve 21 | ``` 22 | 23 | Visit [http://localhost:8007/](http://localhost:8007/) and click around to some 24 | different items. If you then go offline then app will render items from 25 | localStorage. 26 | -------------------------------------------------------------------------------- /examples/pwa/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /examples/pwa/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swrv-pwa-example", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "core-js": "^3.6.4", 12 | "register-service-worker": "^1.6.2", 13 | "swrv": "latest", 14 | "vue": "^3.2.40" 15 | }, 16 | "devDependencies": { 17 | "@babel/core": "^7.12.16", 18 | "@babel/eslint-parser": "^7.12.16", 19 | "@vue/cli-plugin-babel": "~5.0.8", 20 | "@vue/cli-plugin-eslint": "~5.0.8", 21 | "@vue/cli-plugin-pwa": "~5.0.8", 22 | "@vue/cli-service": "~5.0.8", 23 | "@vue/compiler-sfc": "^3.2.40", 24 | "eslint": "^7.32.0", 25 | "eslint-plugin-vue": "^8.0.3" 26 | }, 27 | "eslintConfig": { 28 | "root": true, 29 | "env": { 30 | "node": true 31 | }, 32 | "extends": [ 33 | "plugin:vue/essential", 34 | "eslint:recommended" 35 | ], 36 | "parserOptions": { 37 | "parser": "@babel/eslint-parser" 38 | }, 39 | "rules": {} 40 | }, 41 | "browserslist": [ 42 | "> 1%", 43 | "last 2 versions" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /examples/pwa/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kong/swrv/790a22cff70218c69020b1ef434e01cd3ec3ac5c/examples/pwa/public/favicon.ico -------------------------------------------------------------------------------- /examples/pwa/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= htmlWebpackPlugin.options.title %> 10 | 11 | 12 | 13 | 14 | 18 |
19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/pwa/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /examples/pwa/src/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 30 | 31 | 39 | -------------------------------------------------------------------------------- /examples/pwa/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kong/swrv/790a22cff70218c69020b1ef434e01cd3ec3ac5c/examples/pwa/src/assets/logo.png -------------------------------------------------------------------------------- /examples/pwa/src/components/TodoItem.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 30 | -------------------------------------------------------------------------------- /examples/pwa/src/components/TodosList.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 46 | 47 | 48 | 64 | -------------------------------------------------------------------------------- /examples/pwa/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import './registerServiceWorker' 4 | 5 | const app = createApp(App) 6 | 7 | app.mount('#app') 8 | -------------------------------------------------------------------------------- /examples/pwa/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import { register } from 'register-service-worker' 4 | 5 | if (process.env.NODE_ENV === 'production') { 6 | register(`${process.env.BASE_URL}service-worker.js`, { 7 | ready () { 8 | console.log( 9 | 'App is being served from cache by a service worker.\n' + 10 | 'For more details, visit https://goo.gl/AFskqB' 11 | ) 12 | }, 13 | registered () { 14 | console.log('Service worker has been registered.') 15 | }, 16 | cached () { 17 | console.log('Content has been cached for offline use.') 18 | }, 19 | updatefound () { 20 | console.log('New content is downloading.') 21 | }, 22 | updated () { 23 | console.log('New content is available; please refresh.') 24 | }, 25 | offline () { 26 | console.log('No internet connection found. App is running in offline mode.') 27 | }, 28 | error (error) { 29 | console.error('Error during service worker registration:', error) 30 | } 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /examples/pwa/src/useTodos.js: -------------------------------------------------------------------------------- 1 | import useSWRV from 'swrv' 2 | import LocalStorageCache from '../../../esm/cache/adapters/localStorage' 3 | 4 | export default function useTodos (path) { 5 | const { data, error } = useSWRV(path, path => fetch(`https://jsonplaceholder.typicode.com${path}`).then(res => res.json()), { 6 | cache: new LocalStorageCache('swrv'), 7 | shouldRetryOnError: false 8 | }) 9 | 10 | return { 11 | data, 12 | error 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /examples/ssr-nuxt/Post.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 45 | -------------------------------------------------------------------------------- /examples/ssr-nuxt/README.md: -------------------------------------------------------------------------------- 1 | # SSR-Nuxt Example 2 | 3 | > Note: This example is no longer functional now that SSR support is broken as of the Vue 2.7 upgrade 4 | 5 | Pagination example server side rendering api responses using 6 | [Nuxt](https://nuxtjs.org/) paginating through blog posts. 7 | 8 | ```sh 9 | yarn install 10 | ``` 11 | 12 | Run app locally with hot reloading: 13 | 14 | ```sh 15 | yarn dev 16 | ``` 17 | -------------------------------------------------------------------------------- /examples/ssr-nuxt/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 43 | 44 | 53 | -------------------------------------------------------------------------------- /examples/ssr-nuxt/main.css: -------------------------------------------------------------------------------- 1 | .fade-enter-active, .fade-leave-active { 2 | transition: opacity 0s; 3 | } 4 | .fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ { 5 | opacity: 0; 6 | } 7 | -------------------------------------------------------------------------------- /examples/ssr-nuxt/nuxt.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | /* 3 | ** Plugins to load before mounting the App 4 | ** Doc: https://nuxtjs.org/guide/plugins 5 | */ 6 | plugins: ['@/plugins/vue-composition-api.js'], 7 | css: [ 8 | '~/main.css' 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /examples/ssr-nuxt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swrv-ssr-nuxt-example", 3 | "scripts": { 4 | "dev": "nuxt --universal", 5 | "build": "nuxt build", 6 | "start": "nuxt start", 7 | "generate": "nuxt generate" 8 | }, 9 | "dependencies": { 10 | "nuxt": "^2.11.0", 11 | "swrv": "latest" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/ssr-nuxt/pages/_id.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /examples/ssr-nuxt/pages/index.vue: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /examples/ssr-nuxt/plugins/vue-composition-api.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueCompositionApi from '@vue/composition-api' 3 | 4 | Vue.use(VueCompositionApi) 5 | -------------------------------------------------------------------------------- /examples/vite/App.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 22 | 23 | 32 | -------------------------------------------------------------------------------- /examples/vite/README.md: -------------------------------------------------------------------------------- 1 | # Vite Example 2 | 3 | Using [Vite](https://github.com/vitejs/vite) + [Vue 3](https://github.com/vuejs/vue-next) 4 | 5 | Install dependencies 6 | 7 | ```sh 8 | yarn install 9 | ``` 10 | 11 | Run app: 12 | 13 | ```sh 14 | yarn dev 15 | ``` 16 | -------------------------------------------------------------------------------- /examples/vite/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 39 | -------------------------------------------------------------------------------- /examples/vite/index.html: -------------------------------------------------------------------------------- 1 |
2 | 8 | -------------------------------------------------------------------------------- /examples/vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swrv-vite-example", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build" 7 | }, 8 | "dependencies": { 9 | "swrv": "beta", 10 | "vue": "^3.2.37" 11 | }, 12 | "devDependencies": { 13 | "@vitejs/plugin-vue": "^2.3.3", 14 | "@vue/compiler-sfc": "^3.2.27", 15 | "vite": "^2.9.14" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/vite/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()] 7 | }) 8 | -------------------------------------------------------------------------------- /examples/vite/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@babel/parser@^7.16.4": 6 | version "7.18.11" 7 | resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.11.tgz#68bb07ab3d380affa9a3f96728df07969645d2d9" 8 | integrity sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ== 9 | 10 | "@esbuild/linux-loong64@0.14.54": 11 | version "0.14.54" 12 | resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz#de2a4be678bd4d0d1ffbb86e6de779cde5999028" 13 | integrity sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw== 14 | 15 | "@vitejs/plugin-vue@^2.3.3": 16 | version "2.3.3" 17 | resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-2.3.3.tgz#fbf80cc039b82ac21a1acb0f0478de8f61fbf600" 18 | integrity sha512-SmQLDyhz+6lGJhPELsBdzXGc+AcaT8stgkbiTFGpXPe8Tl1tJaBw1A6pxDqDuRsVkD8uscrkx3hA7QDOoKYtyw== 19 | 20 | "@vue/compiler-core@3.2.37": 21 | version "3.2.37" 22 | resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.37.tgz#b3c42e04c0e0f2c496ff1784e543fbefe91e215a" 23 | integrity sha512-81KhEjo7YAOh0vQJoSmAD68wLfYqJvoiD4ulyedzF+OEk/bk6/hx3fTNVfuzugIIaTrOx4PGx6pAiBRe5e9Zmg== 24 | dependencies: 25 | "@babel/parser" "^7.16.4" 26 | "@vue/shared" "3.2.37" 27 | estree-walker "^2.0.2" 28 | source-map "^0.6.1" 29 | 30 | "@vue/compiler-dom@3.2.37": 31 | version "3.2.37" 32 | resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.37.tgz#10d2427a789e7c707c872da9d678c82a0c6582b5" 33 | integrity sha512-yxJLH167fucHKxaqXpYk7x8z7mMEnXOw3G2q62FTkmsvNxu4FQSu5+3UMb+L7fjKa26DEzhrmCxAgFLLIzVfqQ== 34 | dependencies: 35 | "@vue/compiler-core" "3.2.37" 36 | "@vue/shared" "3.2.37" 37 | 38 | "@vue/compiler-sfc@3.2.37", "@vue/compiler-sfc@^3.2.27": 39 | version "3.2.37" 40 | resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.37.tgz#3103af3da2f40286edcd85ea495dcb35bc7f5ff4" 41 | integrity sha512-+7i/2+9LYlpqDv+KTtWhOZH+pa8/HnX/905MdVmAcI/mPQOBwkHHIzrsEsucyOIZQYMkXUiTkmZq5am/NyXKkg== 42 | dependencies: 43 | "@babel/parser" "^7.16.4" 44 | "@vue/compiler-core" "3.2.37" 45 | "@vue/compiler-dom" "3.2.37" 46 | "@vue/compiler-ssr" "3.2.37" 47 | "@vue/reactivity-transform" "3.2.37" 48 | "@vue/shared" "3.2.37" 49 | estree-walker "^2.0.2" 50 | magic-string "^0.25.7" 51 | postcss "^8.1.10" 52 | source-map "^0.6.1" 53 | 54 | "@vue/compiler-ssr@3.2.37": 55 | version "3.2.37" 56 | resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.37.tgz#4899d19f3a5fafd61524a9d1aee8eb0505313cff" 57 | integrity sha512-7mQJD7HdXxQjktmsWp/J67lThEIcxLemz1Vb5I6rYJHR5vI+lON3nPGOH3ubmbvYGt8xEUaAr1j7/tIFWiEOqw== 58 | dependencies: 59 | "@vue/compiler-dom" "3.2.37" 60 | "@vue/shared" "3.2.37" 61 | 62 | "@vue/reactivity-transform@3.2.37": 63 | version "3.2.37" 64 | resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.37.tgz#0caa47c4344df4ae59f5a05dde2a8758829f8eca" 65 | integrity sha512-IWopkKEb+8qpu/1eMKVeXrK0NLw9HicGviJzhJDEyfxTR9e1WtpnnbYkJWurX6WwoFP0sz10xQg8yL8lgskAZg== 66 | dependencies: 67 | "@babel/parser" "^7.16.4" 68 | "@vue/compiler-core" "3.2.37" 69 | "@vue/shared" "3.2.37" 70 | estree-walker "^2.0.2" 71 | magic-string "^0.25.7" 72 | 73 | "@vue/reactivity@3.2.37": 74 | version "3.2.37" 75 | resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.37.tgz#5bc3847ac58828e2b78526e08219e0a1089f8848" 76 | integrity sha512-/7WRafBOshOc6m3F7plwzPeCu/RCVv9uMpOwa/5PiY1Zz+WLVRWiy0MYKwmg19KBdGtFWsmZ4cD+LOdVPcs52A== 77 | dependencies: 78 | "@vue/shared" "3.2.37" 79 | 80 | "@vue/runtime-core@3.2.37": 81 | version "3.2.37" 82 | resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.37.tgz#7ba7c54bb56e5d70edfc2f05766e1ca8519966e3" 83 | integrity sha512-JPcd9kFyEdXLl/i0ClS7lwgcs0QpUAWj+SKX2ZC3ANKi1U4DOtiEr6cRqFXsPwY5u1L9fAjkinIdB8Rz3FoYNQ== 84 | dependencies: 85 | "@vue/reactivity" "3.2.37" 86 | "@vue/shared" "3.2.37" 87 | 88 | "@vue/runtime-dom@3.2.37": 89 | version "3.2.37" 90 | resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.37.tgz#002bdc8228fa63949317756fb1e92cdd3f9f4bbd" 91 | integrity sha512-HimKdh9BepShW6YozwRKAYjYQWg9mQn63RGEiSswMbW+ssIht1MILYlVGkAGGQbkhSh31PCdoUcfiu4apXJoPw== 92 | dependencies: 93 | "@vue/runtime-core" "3.2.37" 94 | "@vue/shared" "3.2.37" 95 | csstype "^2.6.8" 96 | 97 | "@vue/server-renderer@3.2.37": 98 | version "3.2.37" 99 | resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.37.tgz#840a29c8dcc29bddd9b5f5ffa22b95c0e72afdfc" 100 | integrity sha512-kLITEJvaYgZQ2h47hIzPh2K3jG8c1zCVbp/o/bzQOyvzaKiCquKS7AaioPI28GNxIsE/zSx+EwWYsNxDCX95MA== 101 | dependencies: 102 | "@vue/compiler-ssr" "3.2.37" 103 | "@vue/shared" "3.2.37" 104 | 105 | "@vue/shared@3.2.37": 106 | version "3.2.37" 107 | resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.37.tgz#8e6adc3f2759af52f0e85863dfb0b711ecc5c702" 108 | integrity sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw== 109 | 110 | csstype@^2.6.8: 111 | version "2.6.10" 112 | resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.10.tgz#e63af50e66d7c266edb6b32909cfd0aabe03928b" 113 | integrity sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w== 114 | 115 | esbuild-android-64@0.14.54: 116 | version "0.14.54" 117 | resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz#505f41832884313bbaffb27704b8bcaa2d8616be" 118 | integrity sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ== 119 | 120 | esbuild-android-arm64@0.14.54: 121 | version "0.14.54" 122 | resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz#8ce69d7caba49646e009968fe5754a21a9871771" 123 | integrity sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg== 124 | 125 | esbuild-darwin-64@0.14.54: 126 | version "0.14.54" 127 | resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz#24ba67b9a8cb890a3c08d9018f887cc221cdda25" 128 | integrity sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug== 129 | 130 | esbuild-darwin-arm64@0.14.54: 131 | version "0.14.54" 132 | resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz#3f7cdb78888ee05e488d250a2bdaab1fa671bf73" 133 | integrity sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw== 134 | 135 | esbuild-freebsd-64@0.14.54: 136 | version "0.14.54" 137 | resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz#09250f997a56ed4650f3e1979c905ffc40bbe94d" 138 | integrity sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg== 139 | 140 | esbuild-freebsd-arm64@0.14.54: 141 | version "0.14.54" 142 | resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz#bafb46ed04fc5f97cbdb016d86947a79579f8e48" 143 | integrity sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q== 144 | 145 | esbuild-linux-32@0.14.54: 146 | version "0.14.54" 147 | resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz#e2a8c4a8efdc355405325033fcebeb941f781fe5" 148 | integrity sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw== 149 | 150 | esbuild-linux-64@0.14.54: 151 | version "0.14.54" 152 | resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz#de5fdba1c95666cf72369f52b40b03be71226652" 153 | integrity sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg== 154 | 155 | esbuild-linux-arm64@0.14.54: 156 | version "0.14.54" 157 | resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz#dae4cd42ae9787468b6a5c158da4c84e83b0ce8b" 158 | integrity sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig== 159 | 160 | esbuild-linux-arm@0.14.54: 161 | version "0.14.54" 162 | resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz#a2c1dff6d0f21dbe8fc6998a122675533ddfcd59" 163 | integrity sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw== 164 | 165 | esbuild-linux-mips64le@0.14.54: 166 | version "0.14.54" 167 | resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz#d9918e9e4cb972f8d6dae8e8655bf9ee131eda34" 168 | integrity sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw== 169 | 170 | esbuild-linux-ppc64le@0.14.54: 171 | version "0.14.54" 172 | resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz#3f9a0f6d41073fb1a640680845c7de52995f137e" 173 | integrity sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ== 174 | 175 | esbuild-linux-riscv64@0.14.54: 176 | version "0.14.54" 177 | resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz#618853c028178a61837bc799d2013d4695e451c8" 178 | integrity sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg== 179 | 180 | esbuild-linux-s390x@0.14.54: 181 | version "0.14.54" 182 | resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz#d1885c4c5a76bbb5a0fe182e2c8c60eb9e29f2a6" 183 | integrity sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA== 184 | 185 | esbuild-netbsd-64@0.14.54: 186 | version "0.14.54" 187 | resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz#69ae917a2ff241b7df1dbf22baf04bd330349e81" 188 | integrity sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w== 189 | 190 | esbuild-openbsd-64@0.14.54: 191 | version "0.14.54" 192 | resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz#db4c8495287a350a6790de22edea247a57c5d47b" 193 | integrity sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw== 194 | 195 | esbuild-sunos-64@0.14.54: 196 | version "0.14.54" 197 | resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz#54287ee3da73d3844b721c21bc80c1dc7e1bf7da" 198 | integrity sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw== 199 | 200 | esbuild-windows-32@0.14.54: 201 | version "0.14.54" 202 | resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz#f8aaf9a5667630b40f0fb3aa37bf01bbd340ce31" 203 | integrity sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w== 204 | 205 | esbuild-windows-64@0.14.54: 206 | version "0.14.54" 207 | resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz#bf54b51bd3e9b0f1886ffdb224a4176031ea0af4" 208 | integrity sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ== 209 | 210 | esbuild-windows-arm64@0.14.54: 211 | version "0.14.54" 212 | resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz#937d15675a15e4b0e4fafdbaa3a01a776a2be982" 213 | integrity sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg== 214 | 215 | esbuild@^0.14.27: 216 | version "0.14.54" 217 | resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.54.tgz#8b44dcf2b0f1a66fc22459943dccf477535e9aa2" 218 | integrity sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA== 219 | optionalDependencies: 220 | "@esbuild/linux-loong64" "0.14.54" 221 | esbuild-android-64 "0.14.54" 222 | esbuild-android-arm64 "0.14.54" 223 | esbuild-darwin-64 "0.14.54" 224 | esbuild-darwin-arm64 "0.14.54" 225 | esbuild-freebsd-64 "0.14.54" 226 | esbuild-freebsd-arm64 "0.14.54" 227 | esbuild-linux-32 "0.14.54" 228 | esbuild-linux-64 "0.14.54" 229 | esbuild-linux-arm "0.14.54" 230 | esbuild-linux-arm64 "0.14.54" 231 | esbuild-linux-mips64le "0.14.54" 232 | esbuild-linux-ppc64le "0.14.54" 233 | esbuild-linux-riscv64 "0.14.54" 234 | esbuild-linux-s390x "0.14.54" 235 | esbuild-netbsd-64 "0.14.54" 236 | esbuild-openbsd-64 "0.14.54" 237 | esbuild-sunos-64 "0.14.54" 238 | esbuild-windows-32 "0.14.54" 239 | esbuild-windows-64 "0.14.54" 240 | esbuild-windows-arm64 "0.14.54" 241 | 242 | estree-walker@^2.0.2: 243 | version "2.0.2" 244 | resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" 245 | integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== 246 | 247 | fsevents@~2.3.2: 248 | version "2.3.2" 249 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" 250 | integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== 251 | 252 | function-bind@^1.1.1: 253 | version "1.1.1" 254 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" 255 | integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== 256 | 257 | has@^1.0.3: 258 | version "1.0.3" 259 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" 260 | integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== 261 | dependencies: 262 | function-bind "^1.1.1" 263 | 264 | is-core-module@^2.9.0: 265 | version "2.10.0" 266 | resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.10.0.tgz#9012ede0a91c69587e647514e1d5277019e728ed" 267 | integrity sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg== 268 | dependencies: 269 | has "^1.0.3" 270 | 271 | magic-string@^0.25.7: 272 | version "0.25.7" 273 | resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" 274 | integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== 275 | dependencies: 276 | sourcemap-codec "^1.4.4" 277 | 278 | nanoid@^3.3.4: 279 | version "3.3.4" 280 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" 281 | integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== 282 | 283 | path-parse@^1.0.7: 284 | version "1.0.7" 285 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" 286 | integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== 287 | 288 | picocolors@^1.0.0: 289 | version "1.0.0" 290 | resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" 291 | integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== 292 | 293 | postcss@^8.1.10, postcss@^8.4.13: 294 | version "8.4.16" 295 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.16.tgz#33a1d675fac39941f5f445db0de4db2b6e01d43c" 296 | integrity sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ== 297 | dependencies: 298 | nanoid "^3.3.4" 299 | picocolors "^1.0.0" 300 | source-map-js "^1.0.2" 301 | 302 | resolve@^1.22.0: 303 | version "1.22.1" 304 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" 305 | integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== 306 | dependencies: 307 | is-core-module "^2.9.0" 308 | path-parse "^1.0.7" 309 | supports-preserve-symlinks-flag "^1.0.0" 310 | 311 | rollup@^2.59.0: 312 | version "2.77.2" 313 | resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.77.2.tgz#6b6075c55f9cc2040a5912e6e062151e42e2c4e3" 314 | integrity sha512-m/4YzYgLcpMQbxX3NmAqDvwLATZzxt8bIegO78FZLl+lAgKJBd1DRAOeEiZcKOIOPjxE6ewHWHNgGEalFXuz1g== 315 | optionalDependencies: 316 | fsevents "~2.3.2" 317 | 318 | source-map-js@^1.0.2: 319 | version "1.0.2" 320 | resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" 321 | integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== 322 | 323 | source-map@^0.6.1: 324 | version "0.6.1" 325 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 326 | integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 327 | 328 | sourcemap-codec@^1.4.4: 329 | version "1.4.8" 330 | resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" 331 | integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== 332 | 333 | supports-preserve-symlinks-flag@^1.0.0: 334 | version "1.0.0" 335 | resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" 336 | integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== 337 | 338 | swrv@beta: 339 | version "1.0.0-beta.8" 340 | resolved "https://registry.yarnpkg.com/swrv/-/swrv-1.0.0-beta.8.tgz#745723f5ca8a7a7e290e911cf7da34cecaf356d3" 341 | integrity sha512-MsjaMOvZODfM0cess/HhbSrNbAotYinv4vzipLckKYBo/QmrvjNUPGZSRSqByXy/9AjrMRFWo0YanaVPbqADPQ== 342 | 343 | vite@^2.9.14: 344 | version "2.9.14" 345 | resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.14.tgz#c438324c6594afd1050df3777da981dee988bb1b" 346 | integrity sha512-P/UCjSpSMcE54r4mPak55hWAZPlyfS369svib/gpmz8/01L822lMPOJ/RYW6tLCe1RPvMvOsJ17erf55bKp4Hw== 347 | dependencies: 348 | esbuild "^0.14.27" 349 | postcss "^8.4.13" 350 | resolve "^1.22.0" 351 | rollup "^2.59.0" 352 | optionalDependencies: 353 | fsevents "~2.3.2" 354 | 355 | vue@^3.2.37: 356 | version "3.2.37" 357 | resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.37.tgz#da220ccb618d78579d25b06c7c21498ca4e5452e" 358 | integrity sha512-bOKEZxrm8Eh+fveCqS1/NkG/n6aMidsI6hahas7pa0w/l7jkbssJVsRhVDs07IdDq7h9KHswZOgItnwJAgtVtQ== 359 | dependencies: 360 | "@vue/compiler-dom" "3.2.37" 361 | "@vue/compiler-sfc" "3.2.37" 362 | "@vue/runtime-dom" "3.2.37" 363 | "@vue/server-renderer" "3.2.37" 364 | "@vue/shared" "3.2.37" 365 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel', 3 | testMatch: [ 4 | '/tests/**/*.spec.[jt]s?(x)' 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kong/swrv/790a22cff70218c69020b1ef434e01cd3ec3ac5c/logo.png -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | base = "/" 3 | command = "yarn docs:build" 4 | publish = "docs/.vitepress/dist" 5 | environment = { NODE_VERSION = "16.16.0", YARN_FLAGS="--frozen-lockfile --ignore-optional" } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swrv", 3 | "version": "1.1.0", 4 | "scripts": { 5 | "build": "yarn build:esm && yarn build:cjs", 6 | "lint": "vue-cli-service lint", 7 | "ac": "all-contributors", 8 | "build:cjs": "tsc --module CommonJs -p tsconfig.build.json", 9 | "build:esm": "tsc --module ES6 --outDir esm -p tsconfig.build.json", 10 | "test": "vue-cli-service test:unit", 11 | "test:compat": "./tests/test-compat-all.sh", 12 | "types:check": "tsc --noEmit", 13 | "docs:dev": "vitepress dev docs", 14 | "docs:build": "vitepress build docs" 15 | }, 16 | "main": "./dist/index.js", 17 | "module": "./esm/index.js", 18 | "files": [ 19 | "dist/**", 20 | "esm/**", 21 | "src/**" 22 | ], 23 | "devDependencies": { 24 | "@types/jest": "^24.0.19", 25 | "@typescript-eslint/eslint-plugin": "^5.35.1", 26 | "@typescript-eslint/parser": "^5.35.1", 27 | "@vue/cli-plugin-babel": "~5.0.8", 28 | "@vue/cli-plugin-eslint": "~5.0.8", 29 | "@vue/cli-plugin-typescript": "~5.0.8", 30 | "@vue/cli-plugin-unit-jest": "~5.0.8", 31 | "@vue/cli-service": "~5.0.8", 32 | "@vue/compiler-sfc": "^3.3.4", 33 | "@vue/eslint-config-standard": "^6.1.0", 34 | "@vue/eslint-config-typescript": "^9.1.0", 35 | "@vue/test-utils": "^2.1.0", 36 | "@vue/vue3-jest": "^29.1.1", 37 | "all-contributors-cli": "^6.20.0", 38 | "babel-loader": "^8.2.5", 39 | "eslint": "^7.32.0", 40 | "eslint-plugin-import": "^2.25.3", 41 | "eslint-plugin-node": "^11.1.0", 42 | "eslint-plugin-promise": "^5.1.0", 43 | "eslint-plugin-vue": "^8.7.1", 44 | "file-loader": "^5.0.2", 45 | "jest": "^27.1.0", 46 | "jest-date-mock": "^1.0.8", 47 | "ts-jest": "^27.0.4", 48 | "typescript": "~4.5.5", 49 | "vitepress": "^1.0.0-alpha.61", 50 | "vue": "^3.3.4", 51 | "vue-template-compiler": "^2.7.14", 52 | "webpack": "~4.0.0" 53 | }, 54 | "peerDependencies": { 55 | "vue": ">=3.2.26 < 4" 56 | }, 57 | "repository": { 58 | "type": "git", 59 | "url": "https://github.com/Kong/swrv.git" 60 | }, 61 | "license": "Apache-2.0", 62 | "types": "./dist/index.d.ts", 63 | "volta": { 64 | "node": "16.16.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/cache/adapters/localStorage.ts: -------------------------------------------------------------------------------- 1 | import SWRVCache, { ICacheItem } from '..' 2 | 3 | /** 4 | * LocalStorage cache adapter for swrv data cache. 5 | * https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage 6 | */ 7 | export default class LocalStorageCache extends SWRVCache { 8 | private STORAGE_KEY 9 | 10 | constructor (key = 'swrv', ttl = 0) { 11 | super(ttl) 12 | this.STORAGE_KEY = key 13 | } 14 | 15 | private encode (storage) { return JSON.stringify(storage) } 16 | private decode (storage) { return JSON.parse(storage) } 17 | 18 | get (k) { 19 | const item = localStorage.getItem(this.STORAGE_KEY) 20 | if (item) { 21 | const _key = this.serializeKey(k) 22 | const itemParsed: ICacheItem = JSON.parse(item)[_key] 23 | 24 | if (itemParsed?.expiresAt === null) { 25 | itemParsed.expiresAt = Infinity // localStorage sets Infinity to 'null' 26 | } 27 | 28 | return itemParsed 29 | } 30 | 31 | return undefined 32 | } 33 | 34 | set (k: string, v: any, ttl: number) { 35 | let payload = {} 36 | const _key = this.serializeKey(k) 37 | const timeToLive = ttl || this.ttl 38 | const storage = localStorage.getItem(this.STORAGE_KEY) 39 | const now = Date.now() 40 | const item = { 41 | data: v, 42 | createdAt: now, 43 | expiresAt: timeToLive ? now + timeToLive : Infinity 44 | } 45 | 46 | if (storage) { 47 | payload = this.decode(storage) 48 | payload[_key] = item 49 | } else { 50 | payload = { [_key]: item } 51 | } 52 | 53 | this.dispatchExpire(timeToLive, item, _key) 54 | localStorage.setItem(this.STORAGE_KEY, this.encode(payload)) 55 | } 56 | 57 | dispatchExpire (ttl, item, serializedKey) { 58 | ttl && setTimeout(() => { 59 | const current = Date.now() 60 | const hasExpired = current >= item.expiresAt 61 | if (hasExpired) this.delete(serializedKey) 62 | }, ttl) 63 | } 64 | 65 | delete (serializedKey: string) { 66 | const storage = localStorage.getItem(this.STORAGE_KEY) 67 | let payload = {} 68 | 69 | if (storage) { 70 | payload = this.decode(storage) 71 | delete payload[serializedKey] 72 | } 73 | 74 | localStorage.setItem(this.STORAGE_KEY, this.encode(payload)) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/cache/index.ts: -------------------------------------------------------------------------------- 1 | import { IKey } from '../types' 2 | import hash from '../lib/hash' 3 | export interface ICacheItem { 4 | data: Data, 5 | createdAt: number, 6 | expiresAt: number 7 | } 8 | 9 | function serializeKeyDefault (key: IKey): string { 10 | if (typeof key === 'function') { 11 | try { 12 | key = key() 13 | } catch (err) { 14 | // dependencies not ready 15 | key = '' 16 | } 17 | } 18 | 19 | if (Array.isArray(key)) { 20 | key = hash(key) 21 | } else { 22 | // convert null to '' 23 | key = String(key || '') 24 | } 25 | 26 | return key 27 | } 28 | 29 | export default class SWRVCache { 30 | protected ttl: number 31 | private items?: Map> 32 | 33 | constructor (ttl = 0) { 34 | this.items = new Map() 35 | this.ttl = ttl 36 | } 37 | 38 | serializeKey (key: IKey): string { 39 | return serializeKeyDefault(key) 40 | } 41 | 42 | get (k: string): ICacheItem { 43 | const _key = this.serializeKey(k) 44 | return this.items.get(_key) 45 | } 46 | 47 | set (k: string, v: any, ttl: number) { 48 | const _key = this.serializeKey(k) 49 | const timeToLive = ttl || this.ttl 50 | const now = Date.now() 51 | const item = { 52 | data: v, 53 | createdAt: now, 54 | expiresAt: timeToLive ? now + timeToLive : Infinity 55 | } 56 | 57 | this.dispatchExpire(timeToLive, item, _key) 58 | this.items.set(_key, item) 59 | } 60 | 61 | dispatchExpire (ttl, item, serializedKey) { 62 | ttl && setTimeout(() => { 63 | const current = Date.now() 64 | const hasExpired = current >= item.expiresAt 65 | if (hasExpired) this.delete(serializedKey) 66 | }, ttl) 67 | } 68 | 69 | delete (serializedKey: string) { 70 | this.items.delete(serializedKey) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import SWRVCache from './cache' 2 | import useSWRV, { mutate } from './use-swrv' 3 | 4 | export { 5 | IConfig 6 | } from './types' 7 | export { mutate, SWRVCache } 8 | export default useSWRV 9 | -------------------------------------------------------------------------------- /src/lib/hash.ts: -------------------------------------------------------------------------------- 1 | // From https://github.com/vercel/swr/blob/master/src/libs/hash.ts 2 | // use WeakMap to store the object->key mapping 3 | // so the objects can be garbage collected. 4 | // WeakMap uses a hashtable under the hood, so the lookup 5 | // complexity is almost O(1). 6 | const table = new WeakMap() 7 | 8 | // counter of the key 9 | let counter = 0 10 | 11 | // hashes an array of objects and returns a string 12 | export default function hash (args: any[]): string { 13 | if (!args.length) return '' 14 | let key = 'arg' 15 | for (let i = 0; i < args.length; ++i) { 16 | let _hash 17 | if ( 18 | args[i] === null || 19 | (typeof args[i] !== 'object' && typeof args[i] !== 'function') 20 | ) { 21 | // need to consider the case that args[i] is a string: 22 | // args[i] _hash 23 | // "undefined" -> '"undefined"' 24 | // undefined -> 'undefined' 25 | // 123 -> '123' 26 | // null -> 'null' 27 | // "null" -> '"null"' 28 | if (typeof args[i] === 'string') { 29 | _hash = '"' + args[i] + '"' 30 | } else { 31 | _hash = String(args[i]) 32 | } 33 | } else { 34 | if (!table.has(args[i])) { 35 | _hash = counter 36 | table.set(args[i], counter++) 37 | } else { 38 | _hash = table.get(args[i]) 39 | } 40 | } 41 | key += '@' + _hash 42 | } 43 | return key 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/web-preset.ts: -------------------------------------------------------------------------------- 1 | function isOnline (): boolean { 2 | if (typeof navigator.onLine !== 'undefined') { 3 | return navigator.onLine 4 | } 5 | // always assume it's online 6 | return true 7 | } 8 | 9 | function isDocumentVisible (): boolean { 10 | if ( 11 | typeof document !== 'undefined' && 12 | typeof document.visibilityState !== 'undefined' 13 | ) { 14 | return document.visibilityState !== 'hidden' 15 | } 16 | // always assume it's visible 17 | return true 18 | } 19 | 20 | const fetcher = url => fetch(url).then(res => res.json()) 21 | 22 | export default { 23 | isOnline, 24 | isDocumentVisible, 25 | fetcher 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { Ref, WatchSource } from 'vue' 2 | import SWRVCache from './cache' 3 | import LocalStorageCache from './cache/adapters/localStorage' 4 | 5 | export type fetcherFn = (...args: any) => Data | Promise 6 | 7 | export interface IConfig< 8 | Data = any, 9 | Fn extends fetcherFn = fetcherFn 10 | > { 11 | refreshInterval?: number 12 | cache?: LocalStorageCache | SWRVCache 13 | dedupingInterval?: number 14 | ttl?: number 15 | serverTTL?: number 16 | revalidateOnFocus?: boolean 17 | revalidateDebounce?: number 18 | shouldRetryOnError?: boolean 19 | errorRetryInterval?: number 20 | errorRetryCount?: number 21 | fetcher?: Fn, 22 | isOnline?: () => boolean 23 | isDocumentVisible?: () => boolean 24 | } 25 | 26 | export interface revalidateOptions { 27 | shouldRetryOnError?: boolean, 28 | errorRetryCount?: number, 29 | forceRevalidate?: boolean, 30 | } 31 | 32 | export interface IResponse { 33 | data: Ref 34 | error: Ref 35 | isValidating: Ref 36 | isLoading: Ref 37 | mutate: (data?: fetcherFn, opts?: revalidateOptions) => Promise 38 | } 39 | 40 | export type keyType = string | any[] | null | undefined 41 | 42 | export type IKey = keyType | WatchSource 43 | -------------------------------------------------------------------------------- /src/use-swrv.ts: -------------------------------------------------------------------------------- 1 | /** ____ 2 | *--------------/ \.------------------/ 3 | * / swrv \. / // 4 | * / / /\. / // 5 | * / _____/ / \. / 6 | * / / ____/ . \. / 7 | * / \ \_____ \. / 8 | * / . \_____ \ \ / // 9 | * \ _____/ / ./ / // 10 | * \ / _____/ ./ / 11 | * \ / / . ./ / 12 | * \ / / ./ / 13 | * . \/ ./ / // 14 | * \ ./ / // 15 | * \.. / / 16 | * . ||| / 17 | * ||| / 18 | * . ||| / // 19 | * ||| / // 20 | * ||| / 21 | */ 22 | import { 23 | reactive, 24 | watch, 25 | ref, 26 | toRefs, 27 | // isRef, 28 | onMounted, 29 | onUnmounted, 30 | getCurrentInstance, 31 | isReadonly 32 | } from 'vue' 33 | import webPreset from './lib/web-preset' 34 | import SWRVCache from './cache' 35 | import { IConfig, IKey, IResponse, fetcherFn, revalidateOptions } from './types' 36 | 37 | type StateRef = { 38 | data: Data, error: Error, isValidating: boolean, isLoading: boolean, revalidate: Function, key: any 39 | }; 40 | 41 | const DATA_CACHE = new SWRVCache>() 42 | const REF_CACHE = new SWRVCache[]>() 43 | const PROMISES_CACHE = new SWRVCache>() 44 | 45 | const defaultConfig: IConfig = { 46 | cache: DATA_CACHE, 47 | refreshInterval: 0, 48 | ttl: 0, 49 | serverTTL: 1000, 50 | dedupingInterval: 2000, 51 | revalidateOnFocus: true, 52 | revalidateDebounce: 0, 53 | shouldRetryOnError: true, 54 | errorRetryInterval: 5000, 55 | errorRetryCount: 5, 56 | fetcher: webPreset.fetcher, 57 | isOnline: webPreset.isOnline, 58 | isDocumentVisible: webPreset.isDocumentVisible 59 | } 60 | 61 | /** 62 | * Cache the refs for later revalidation 63 | */ 64 | function setRefCache (key: string, theRef: StateRef, ttl: number) { 65 | const refCacheItem = REF_CACHE.get(key) 66 | if (refCacheItem) { 67 | refCacheItem.data.push(theRef) 68 | } else { 69 | // #51 ensures ref cache does not evict too soon 70 | const gracePeriod = 5000 71 | REF_CACHE.set(key, [theRef], ttl > 0 ? ttl + gracePeriod : ttl) 72 | } 73 | } 74 | 75 | function onErrorRetry (revalidate: (any, opts: revalidateOptions) => void, errorRetryCount: number, config: IConfig): void { 76 | if (!config.isDocumentVisible()) { 77 | return 78 | } 79 | 80 | if (config.errorRetryCount !== undefined && errorRetryCount > config.errorRetryCount) { 81 | return 82 | } 83 | 84 | const count = Math.min(errorRetryCount || 0, config.errorRetryCount) 85 | const timeout = count * config.errorRetryInterval 86 | setTimeout(() => { 87 | revalidate(null, { errorRetryCount: count + 1, shouldRetryOnError: true }) 88 | }, timeout) 89 | } 90 | 91 | /** 92 | * Main mutation function for receiving data from promises to change state and 93 | * set data cache 94 | */ 95 | const mutate = async (key: string, res: Promise | Data, cache = DATA_CACHE, ttl = defaultConfig.ttl) => { 96 | let data, error, isValidating 97 | 98 | if (isPromise(res)) { 99 | try { 100 | data = await res 101 | } catch (err) { 102 | error = err 103 | } 104 | } else { 105 | data = res 106 | } 107 | 108 | // eslint-disable-next-line prefer-const 109 | isValidating = false 110 | 111 | const newData = { data, error, isValidating } 112 | if (typeof data !== 'undefined') { 113 | try { 114 | cache.set(key, newData, ttl) 115 | } catch (err) { 116 | console.error('swrv(mutate): failed to set cache', err) 117 | } 118 | } 119 | 120 | /** 121 | * Revalidate all swrv instances with new data 122 | */ 123 | const stateRef = REF_CACHE.get(key) 124 | if (stateRef && stateRef.data.length) { 125 | // This filter fixes #24 race conditions to only update ref data of current 126 | // key, while data cache will continue to be updated if revalidation is 127 | // fired 128 | let refs = stateRef.data.filter(r => r.key === key) 129 | 130 | refs.forEach((r, idx) => { 131 | if (typeof newData.data !== 'undefined') { 132 | r.data = newData.data 133 | } 134 | r.error = newData.error 135 | r.isValidating = newData.isValidating 136 | r.isLoading = newData.isValidating 137 | 138 | const isLast = idx === refs.length - 1 139 | if (!isLast) { 140 | // Clean up refs that belonged to old keys 141 | delete refs[idx] 142 | } 143 | }) 144 | 145 | refs = refs.filter(Boolean) 146 | } 147 | 148 | return newData 149 | } 150 | 151 | /* Stale-While-Revalidate hook to handle fetching, caching, validation, and more... */ 152 | function useSWRV( 153 | key: IKey 154 | ): IResponse 155 | function useSWRV( 156 | key: IKey, 157 | fn: fetcherFn | undefined | null, 158 | config?: IConfig 159 | ): IResponse 160 | function useSWRV (...args): IResponse { 161 | let key: IKey 162 | let fn: fetcherFn | undefined | null 163 | let config: IConfig = { ...defaultConfig } 164 | let unmounted = false 165 | let isHydrated = false 166 | 167 | const instance = getCurrentInstance() as any 168 | const vm = instance?.proxy || instance // https://github.com/vuejs/composition-api/pull/520 169 | if (!vm) { 170 | console.error('Could not get current instance, check to make sure that `useSwrv` is declared in the top level of the setup function.') 171 | return null 172 | } 173 | 174 | const IS_SERVER = vm?.$isServer || false 175 | 176 | // #region ssr 177 | /** 178 | const isSsrHydration = Boolean( 179 | \!IS_SERVER && 180 | vm.$vnode && 181 | vm.$vnode.elm && 182 | vm.$vnode.elm.dataset && 183 | vm.$vnode.elm.dataset.swrvKey) 184 | */ 185 | // #endregion 186 | 187 | if (args.length >= 1) { 188 | key = args[0] 189 | } 190 | if (args.length >= 2) { 191 | fn = args[1] 192 | } 193 | if (args.length > 2) { 194 | config = { 195 | ...config, 196 | ...args[2] 197 | } 198 | } 199 | 200 | const ttl = IS_SERVER ? config.serverTTL : config.ttl 201 | const keyRef = typeof key === 'function' ? (key as any) : ref(key) 202 | 203 | if (typeof fn === 'undefined') { 204 | // use the global fetcher 205 | fn = config.fetcher 206 | } 207 | 208 | let stateRef = null as StateRef 209 | 210 | // #region ssr 211 | // if (isSsrHydration) { 212 | // // component was ssrHydrated, so make the ssr reactive as the initial data 213 | // const swrvState = (window as any).__SWRV_STATE__ || 214 | // ((window as any).__NUXT__ && (window as any).__NUXT__.swrv) || [] 215 | // const swrvKey = +(vm as any).$vnode.elm.dataset.swrvKey 216 | 217 | // if (swrvKey !== undefined && swrvKey !== null) { 218 | // const nodeState = swrvState[swrvKey] || [] 219 | // const instanceState = nodeState[isRef(keyRef) ? keyRef.value : keyRef()] 220 | 221 | // if (instanceState) { 222 | // stateRef = reactive(instanceState) 223 | // isHydrated = true 224 | // } 225 | // } 226 | // } 227 | // #endregion 228 | 229 | if (!stateRef) { 230 | stateRef = reactive({ 231 | data: undefined, 232 | error: undefined, 233 | isValidating: true, 234 | isLoading: true, 235 | key: null 236 | }) as StateRef 237 | } 238 | 239 | /** 240 | * Revalidate the cache, mutate data 241 | */ 242 | const revalidate = async (data?: fetcherFn, opts?: revalidateOptions) => { 243 | const isFirstFetch = stateRef.data === undefined 244 | const keyVal = keyRef.value 245 | if (!keyVal) { return } 246 | 247 | const cacheItem = config.cache.get(keyVal) 248 | const newData = cacheItem && cacheItem.data 249 | 250 | stateRef.isValidating = true 251 | stateRef.isLoading = !newData 252 | if (newData) { 253 | stateRef.data = newData.data 254 | stateRef.error = newData.error 255 | } 256 | 257 | const fetcher = data || fn 258 | if ( 259 | !fetcher || 260 | (!config.isDocumentVisible() && !isFirstFetch) || 261 | (opts?.forceRevalidate !== undefined && !opts?.forceRevalidate) 262 | ) { 263 | stateRef.isValidating = false 264 | stateRef.isLoading = false 265 | return 266 | } 267 | 268 | // Dedupe items that were created in the last interval #76 269 | if (cacheItem) { 270 | const shouldRevalidate = Boolean( 271 | ((Date.now() - cacheItem.createdAt) >= config.dedupingInterval) || opts?.forceRevalidate 272 | ) 273 | 274 | if (!shouldRevalidate) { 275 | stateRef.isValidating = false 276 | stateRef.isLoading = false 277 | return 278 | } 279 | } 280 | 281 | const trigger = async () => { 282 | const promiseFromCache = PROMISES_CACHE.get(keyVal) 283 | if (!promiseFromCache) { 284 | const fetcherArgs = Array.isArray(keyVal) ? keyVal : [keyVal] 285 | const newPromise = fetcher(...fetcherArgs) 286 | PROMISES_CACHE.set(keyVal, newPromise, config.dedupingInterval) 287 | await mutate(keyVal, newPromise, config.cache, ttl) 288 | } else { 289 | await mutate(keyVal, promiseFromCache.data, config.cache, ttl) 290 | } 291 | stateRef.isValidating = false 292 | stateRef.isLoading = false 293 | PROMISES_CACHE.delete(keyVal) 294 | if (stateRef.error !== undefined) { 295 | const shouldRetryOnError = !unmounted && config.shouldRetryOnError && (opts ? opts.shouldRetryOnError : true) 296 | if (shouldRetryOnError) { 297 | onErrorRetry(revalidate, opts ? opts.errorRetryCount : 1, config) 298 | } 299 | } 300 | } 301 | 302 | if (newData && config.revalidateDebounce) { 303 | setTimeout(async () => { 304 | if (!unmounted) { 305 | await trigger() 306 | } 307 | }, config.revalidateDebounce) 308 | } else { 309 | await trigger() 310 | } 311 | } 312 | 313 | const revalidateCall = async () => revalidate(null, { shouldRetryOnError: false }) 314 | let timer = null 315 | /** 316 | * Setup polling 317 | */ 318 | onMounted(() => { 319 | const tick = async () => { 320 | // component might un-mount during revalidate, so do not set a new timeout 321 | // if this is the case, but continue to revalidate since promises can't 322 | // be cancelled and new hook instances might rely on promise/data cache or 323 | // from pre-fetch 324 | if (!stateRef.error && config.isOnline()) { 325 | // if API request errored, we stop polling in this round 326 | // and let the error retry function handle it 327 | await revalidate() 328 | } else { 329 | if (timer) { 330 | clearTimeout(timer) 331 | } 332 | } 333 | 334 | if (config.refreshInterval && !unmounted) { 335 | timer = setTimeout(tick, config.refreshInterval) 336 | } 337 | } 338 | 339 | if (config.refreshInterval) { 340 | timer = setTimeout(tick, config.refreshInterval) 341 | } 342 | if (config.revalidateOnFocus) { 343 | document.addEventListener('visibilitychange', revalidateCall, false) 344 | window.addEventListener('focus', revalidateCall, false) 345 | } 346 | }) 347 | 348 | /** 349 | * Teardown 350 | */ 351 | onUnmounted(() => { 352 | unmounted = true 353 | if (timer) { 354 | clearTimeout(timer) 355 | } 356 | if (config.revalidateOnFocus) { 357 | document.removeEventListener('visibilitychange', revalidateCall, false) 358 | window.removeEventListener('focus', revalidateCall, false) 359 | } 360 | const refCacheItem = REF_CACHE.get(keyRef.value) 361 | if (refCacheItem) { 362 | refCacheItem.data = refCacheItem.data.filter((ref) => ref !== stateRef) 363 | } 364 | }) 365 | 366 | // #region ssr 367 | // if (IS_SERVER) { 368 | // // make sure srwv exists in ssrContext 369 | // let swrvRes = [] 370 | // if (vm.$ssrContext) { 371 | // swrvRes = vm.$ssrContext.swrv = vm.$ssrContext.swrv || swrvRes 372 | // } 373 | 374 | // const ssrKey = swrvRes.length 375 | // if (!vm.$vnode || (vm.$node && !vm.$node.data)) { 376 | // vm.$vnode = { 377 | // data: { attrs: { 'data-swrv-key': ssrKey } } 378 | // } 379 | // } 380 | 381 | // const attrs = (vm.$vnode.data.attrs = vm.$vnode.data.attrs || {}) 382 | // attrs['data-swrv-key'] = ssrKey 383 | 384 | // // Nuxt compatibility 385 | // if (vm.$ssrContext && vm.$ssrContext.nuxt) { 386 | // vm.$ssrContext.nuxt.swrv = swrvRes 387 | // } 388 | 389 | // onServerPrefetch(async () => { 390 | // await revalidate() 391 | 392 | // if (!swrvRes[ssrKey]) swrvRes[ssrKey] = {} 393 | 394 | // swrvRes[ssrKey][keyRef.value] = { 395 | // data: stateRef.data, 396 | // error: stateRef.error, 397 | // isValidating: stateRef.isValidating 398 | // } 399 | // }) 400 | // } 401 | // #endregion 402 | 403 | /** 404 | * Revalidate when key dependencies change 405 | */ 406 | try { 407 | watch(keyRef, (val) => { 408 | if (!isReadonly(keyRef)) { 409 | keyRef.value = val 410 | } 411 | stateRef.key = val 412 | stateRef.isValidating = Boolean(val) 413 | setRefCache(keyRef.value, stateRef, ttl) 414 | 415 | if (!IS_SERVER && !isHydrated && keyRef.value) { 416 | revalidate() 417 | } 418 | isHydrated = false 419 | }, { 420 | immediate: true 421 | }) 422 | } catch { 423 | // do nothing 424 | } 425 | 426 | const res: IResponse = { 427 | ...toRefs(stateRef), 428 | mutate: (data, opts: revalidateOptions) => revalidate(data, { 429 | ...opts, 430 | forceRevalidate: true 431 | }) 432 | } 433 | 434 | return res 435 | } 436 | 437 | function isPromise (p: any): p is Promise { 438 | return p !== null && typeof p === 'object' && typeof p.then === 'function' 439 | } 440 | 441 | export { mutate } 442 | export default useSWRV 443 | -------------------------------------------------------------------------------- /tests/cache.spec.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue' 2 | import { mount } from '@vue/test-utils' 3 | import timeout from './utils/jest-timeout' 4 | import useSWRV from '../src/use-swrv' 5 | import LocalStorageAdapter from '../src/cache/adapters/localStorage' 6 | import { ICacheItem } from '../src/cache' 7 | import tick from './utils/tick' 8 | import { advanceBy, advanceTo } from 'jest-date-mock' 9 | 10 | jest.useFakeTimers() 11 | 12 | describe('cache - adapters', () => { 13 | beforeEach(() => { 14 | localStorage.clear() 15 | }) 16 | 17 | describe('localStorage', () => { 18 | const fetch = url => new Promise((resolve) => { 19 | setTimeout(() => { resolve(`I am a response from ${url}`) }, 100) 20 | }) 21 | 22 | it('saves data cache', async () => { 23 | mount(defineComponent({ 24 | template: '
hello, {{ data }}
', 25 | setup () { 26 | return useSWRV('/api/users', fetch, { 27 | cache: new LocalStorageAdapter() 28 | }) 29 | } 30 | })) 31 | 32 | expect(localStorage.getItem('swrv')).toBeNull() 33 | 34 | await timeout(100) 35 | 36 | const localStorageData: Record> = JSON.parse(localStorage.getItem('swrv')) 37 | 38 | expect(localStorage.getItem('swrv')).toBeDefined() 39 | expect(localStorageData).toHaveProperty('/api/users') 40 | expect(localStorageData['/api/users'].expiresAt).toBeNull() // Infinity shows up as 'null' 41 | expect(localStorageData['/api/users'].createdAt).toBeLessThanOrEqual(Date.now()) 42 | expect(localStorageData['/api/users'].data.data).toBe('I am a response from /api/users') 43 | }) 44 | 45 | it('updates data cache', async () => { 46 | let count = 0 47 | mount(defineComponent({ 48 | template: '
hello, {{ data }}
', 49 | setup () { 50 | return useSWRV('/api/consumers', () => ++count, { 51 | cache: new LocalStorageAdapter(), 52 | refreshInterval: 100, 53 | dedupingInterval: 0 54 | }) 55 | } 56 | })) 57 | 58 | expect(localStorage.getItem('swrv')).toBeDefined() 59 | const checkStorage = (key): Record> => { 60 | return JSON.parse(localStorage.getItem('swrv'))[key] 61 | } 62 | 63 | await timeout(100) 64 | await tick() 65 | expect(checkStorage('/api/consumers').data.data).toBe(1) 66 | 67 | await timeout(100) 68 | await tick() 69 | expect(checkStorage('/api/consumers').data.data).toBe(2) 70 | 71 | await timeout(100) 72 | await tick() 73 | expect(checkStorage('/api/consumers').data.data).toBe(3) 74 | }) 75 | 76 | it('deletes cache item after expiry', async () => { 77 | advanceTo(new Date()) 78 | mount(defineComponent({ 79 | template: '
hello, {{ data }}
', 80 | setup () { 81 | return useSWRV('/api/services', fetch, { 82 | cache: new LocalStorageAdapter('swrv', 200) 83 | }) 84 | } 85 | })) 86 | 87 | expect(localStorage.getItem('swrv')).toBeDefined() 88 | const checkStorage = (key): Record> => { 89 | const db = localStorage.getItem('swrv') 90 | if (!db) { 91 | return undefined 92 | } 93 | return JSON.parse(db)[key] 94 | } 95 | 96 | await timeout(100) 97 | await tick(4) 98 | expect(checkStorage('/api/services').data.data).toBe('I am a response from /api/services') 99 | 100 | // TODO: not sure why only running both these methods works 101 | await advanceBy(200) 102 | await timeout(200) 103 | 104 | await tick() 105 | expect(checkStorage('/api/services')).toBeUndefined() 106 | }) 107 | 108 | it('accepts custom localStorage key', async () => { 109 | mount(defineComponent({ 110 | template: '
hello, {{ data }}
', 111 | setup () { 112 | return useSWRV('/api/some-data', fetch, { 113 | cache: new LocalStorageAdapter('myAppData') 114 | }) 115 | } 116 | })) 117 | 118 | expect(localStorage.getItem('myAppData')).toBeNull() 119 | expect(localStorage.getItem('swrv')).toBeNull() 120 | 121 | await timeout(100) 122 | expect(localStorage.getItem('myAppData')).toBeDefined() 123 | }) 124 | }) 125 | }) 126 | -------------------------------------------------------------------------------- /tests/ssr.spec.ts: -------------------------------------------------------------------------------- 1 | // import Vue from 'vue/dist/vue.runtime.common.js' 2 | // import path from 'path' 3 | // import { createBundleRenderer } from 'vue-server-renderer' 4 | // import { compileWithWebpack } from './utils/compile-with-webpack' 5 | 6 | // Vue.config.devtools = false 7 | // Vue.config.productionTip = false 8 | 9 | // // increase default timeout to account for webpack builds 10 | // jest.setTimeout(10000) 11 | 12 | // export function createRenderer (file, options, cb) { 13 | // compileWithWebpack(file, { 14 | // target: 'node', 15 | // devtool: false, 16 | // output: { 17 | // path: path.resolve(__dirname, 'dist'), 18 | // filename: 'bundle.js', 19 | // libraryTarget: 'umd' 20 | // }, 21 | // externals: [require.resolve('vue/dist/vue.runtime.common.js')] 22 | // }, fs => { 23 | // const bundle = fs.readFileSync(path.resolve(__dirname, 'dist/bundle.js'), 'utf-8') 24 | // const renderer = createBundleRenderer(bundle, options) 25 | // cb(renderer) 26 | // }) 27 | // } 28 | 29 | describe('SSR', () => { 30 | it.skip('should fetch server-side', async () => { 31 | // createRenderer('app.js', {}, renderer => { 32 | // renderer.renderToString({}, (err, res) => { 33 | // expect(err).toBeNull() 34 | // expect(res).toBe('
data:foo
') 35 | // done() 36 | // }) 37 | // }) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /tests/test-compat-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | tests/test-compat.sh "3.2.47" 6 | tests/test-compat.sh "3.3.13" 7 | tests/test-compat.sh "3.4.38" 8 | tests/test-compat.sh "latest" 9 | -------------------------------------------------------------------------------- /tests/test-compat.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | echo "running unit tests with 'vue@$1'" 6 | yarn add -W -D vue@$1 7 | yarn add -W -D @vue/compiler-sfc@$1 8 | 9 | # This is the only way to assure `resolution` field is respected 10 | rm -rf node_modules 11 | yarn install 12 | 13 | # yarn build:esm # needed for ssr 14 | yarn test 15 | -------------------------------------------------------------------------------- /tests/use-swrv.spec.tsx: -------------------------------------------------------------------------------- 1 | import { watch, defineComponent, ref, h, computed } from 'vue' 2 | import { mount } from '@vue/test-utils' 3 | import useSWRV, { mutate } from '../src/use-swrv' 4 | import tick from './utils/tick' 5 | import timeout from './utils/jest-timeout' 6 | import { advanceBy, advanceTo, clear } from 'jest-date-mock' 7 | 8 | // "Mock" the three caches that use-swrv.ts creates so that tests can make assertions about their contents. 9 | let mockDataCache 10 | let mockRefCache 11 | let mockPromisesCache 12 | 13 | jest.mock('../src/cache', () => { 14 | const originalCache = jest.requireActual('../src/cache') 15 | const Cache = originalCache.default 16 | return { 17 | __esModule: true, 18 | default: jest 19 | .fn() 20 | .mockImplementationOnce(() => { 21 | mockDataCache = new Cache() 22 | return mockDataCache 23 | }) 24 | .mockImplementationOnce(() => { 25 | mockRefCache = new Cache() 26 | return mockRefCache 27 | }) 28 | .mockImplementationOnce(function () { 29 | mockPromisesCache = new Cache() 30 | return mockPromisesCache 31 | }) 32 | } 33 | }) 34 | 35 | jest.useFakeTimers() 36 | 37 | const mockFetch = (res?) => { 38 | global.fetch = () => Promise.resolve(null) 39 | const mockFetch = body => Promise.resolve({ json: () => Promise.resolve(body) } as any) 40 | jest.spyOn(window, 'fetch').mockImplementation(body => mockFetch(res || body)) 41 | } 42 | 43 | describe('useSWRV', () => { 44 | it('should return data on hydration when fetch is not a promise', () => { 45 | const fetch = () => 'SWR' 46 | const wrapper = mount(defineComponent({ 47 | template: '
hello, {{ data }}
', 48 | setup () { 49 | return useSWRV('cache-key-not-a-promise', fetch) 50 | } 51 | })) 52 | 53 | expect(wrapper.text()).toBe('hello, SWR') 54 | }) 55 | 56 | it('should return `undefined` on hydration', () => { 57 | // const fetch = () => new Promise(res => setTimeout(() => res('SWR'), 1)) 58 | // const wrapper = mount(defineComponent({ 59 | // template: '
hello, {{ data }}
', 60 | // setup () { 61 | // return useSWRV('cache-key-1', fetch) 62 | // } 63 | // })) 64 | 65 | // expect(wrapper.vm.data).toBe(undefined) 66 | }) 67 | 68 | it('should return data after hydration', async () => { 69 | const wrapper = mount(defineComponent({ 70 | template: '
hello, {{ data }}
', 71 | setup () { 72 | return useSWRV('cache-key-2', () => 'SWR') 73 | } 74 | })) 75 | 76 | await tick(4) 77 | 78 | expect(wrapper.text()).toBe('hello, SWR') 79 | }) 80 | 81 | it('should return data from a promise', async () => { 82 | const wrapper = mount(defineComponent({ 83 | template: '
hello, {{ data }}
', 84 | setup () { 85 | return useSWRV('cache-key-promise', () => new Promise(resolve => resolve('SWR'))) 86 | } 87 | })) 88 | 89 | expect(wrapper.text()).toBe('hello,') 90 | 91 | await tick(2) 92 | 93 | expect(wrapper.text()).toBe('hello, SWR') 94 | }) 95 | 96 | it('should allow functions as key and reuse the cache', async () => { 97 | const wrapper = mount(defineComponent({ 98 | template: '
hello, {{ data }}
', 99 | setup () { 100 | return useSWRV(() => 'cache-key-2', () => 'SWR') 101 | } 102 | })) 103 | 104 | // immediately available via cache without waiting for $nextTick 105 | expect(wrapper.text()).toBe('hello, SWR') 106 | }) 107 | 108 | it('should allow refs (reactive / WatchSource) as key', async () => { 109 | const count = ref('refs:0') 110 | const wrapper = mount(defineComponent({ 111 | template: '', 112 | setup () { 113 | const { data } = useSWRV(count, () => count.value) 114 | 115 | function bumpIt () { 116 | const parts = count.value.split(':') 117 | count.value = `${parts[0]}:${parseInt(parts[1] + 1)}` 118 | } 119 | 120 | return { 121 | bumpIt, 122 | data 123 | } 124 | } 125 | })) 126 | 127 | expect(wrapper.text()).toBe('refs:0') 128 | wrapper.find('button').trigger('click') 129 | await tick(1) 130 | expect(wrapper.text()).toBe('refs:1') 131 | 132 | const wrapper2 = mount(defineComponent({ 133 | template: '
{{ data }}
', 134 | setup () { 135 | return useSWRV(count, () => count.value) 136 | } 137 | })) 138 | 139 | // ref is good for another swrv instance (i.e. object reference works) 140 | expect(wrapper2.text()).toBe('refs:1') 141 | }) 142 | 143 | it('should allow read-only computed key and reuse the cache', async () => { 144 | const wrapper = mount(defineComponent({ 145 | template: '
hello, {{ data }}
', 146 | setup () { 147 | const computedKey = computed(() => 'cache-key-read-only-computed') 148 | return useSWRV(computedKey, () => 'SWR') 149 | } 150 | })) 151 | 152 | // immediately available via cache without waiting for $nextTick 153 | expect(wrapper.text()).toBe('hello, SWR') 154 | }) 155 | 156 | it('should accept object args', async () => { 157 | const obj = { v: 'hello' } 158 | const arr = ['world'] 159 | 160 | const wrapper = mount(defineComponent({ 161 | template: '
{{v1}}, {{v2}}, {{v3}}
', 162 | setup () { 163 | const { data: v1 } = useSWRV(['args-1', obj, arr], (a, b, c) => { 164 | return a + b.v + c[0] 165 | }) 166 | 167 | // reuse the cache 168 | const { data: v2 } = useSWRV(['args-1', obj, arr], () => 'not called!') 169 | 170 | // different object 171 | const { data: v3 } = useSWRV(['args-2', obj, 'world'], (a, b, c) => { 172 | return a + b.v + c 173 | }) 174 | 175 | return { v1, v2, v3 } 176 | } 177 | })) 178 | 179 | expect(wrapper.text()).toBe('args-1helloworld, args-1helloworld, args-2helloworld') 180 | }) 181 | 182 | it('should allow async fetcher functions', async () => { 183 | const wrapper = mount(defineComponent({ 184 | template: '
hello, {{ data }}
', 185 | setup () { 186 | return useSWRV('cache-key-3', () => 187 | new Promise(res => setTimeout(() => res('SWR'), 200)) 188 | ) 189 | } 190 | })) 191 | 192 | expect(wrapper.text()).toBe('hello,') 193 | 194 | timeout(200) 195 | await tick(2) 196 | 197 | expect(wrapper.text()).toBe('hello, SWR') 198 | }) 199 | 200 | it('should dedupe requests by default - in flight promises', async () => { 201 | let count = 0 202 | const fetch = () => { 203 | count++ 204 | return new Promise(res => setTimeout(() => res('SWR'), 200)) 205 | } 206 | 207 | const wrapper = mount(defineComponent({ 208 | template: '
{{v1}}, {{v2}}, {{ validating1 ? \'yes\' : \'no\' }} {{ validating2 ? \'yes\' : \'no\' }}
', 209 | setup () { 210 | const { data: v1, isValidating: validating1 } = useSWRV('cache-key-4', fetch) 211 | const { data: v2, isValidating: validating2 } = useSWRV('cache-key-4', fetch) 212 | return { v1, v2, validating1, validating2 } 213 | } 214 | })) 215 | 216 | expect(wrapper.text()).toBe(', , yes yes') 217 | 218 | timeout(200) 219 | await tick(2) 220 | expect(wrapper.text()).toBe('SWR, SWR, no no') 221 | 222 | // only fetches once 223 | expect(count).toEqual(1) 224 | }) 225 | 226 | it('should dedupe requests by default outside of in flight promises', async () => { 227 | let count = 0 228 | const fetch = () => { 229 | count++ 230 | return new Promise(res => setTimeout(() => res('SWR'), 200)) 231 | } 232 | 233 | const wrapper = mount(defineComponent({ 234 | template: '
{{v1}}, {{v2}}, {{ validating1 ? \'yes\' : \'no\' }} {{ validating2 ? \'yes\' : \'no\' }}
', 235 | setup () { 236 | const { data: v1, isValidating: validating1 } = useSWRV('cache-key-4a', fetch) 237 | const { data: v2, isValidating: validating2 } = useSWRV('cache-key-4a', fetch, { 238 | refreshInterval: 300 239 | }) 240 | return { v1, v2, validating1, validating2 } 241 | } 242 | })) 243 | 244 | expect(wrapper.text()).toBe(', , yes yes') 245 | 246 | timeout(200) 247 | await tick(2) 248 | expect(wrapper.text()).toBe('SWR, SWR, no no') 249 | 250 | timeout(100) 251 | await tick(2) 252 | expect(wrapper.text()).toBe('SWR, SWR, no no') 253 | 254 | timeout(100) 255 | await tick(4) 256 | expect(wrapper.text()).toBe('SWR, SWR, no no') 257 | 258 | expect(count).toEqual(1) 259 | }) 260 | 261 | it('should fetch dependently', async () => { 262 | let count = 0 263 | const loadUser = (): Promise<{ id: number }> => { 264 | return new Promise(res => setTimeout(() => { 265 | count++ 266 | res({ id: 123 }) 267 | }, 1000)) 268 | } 269 | 270 | const loadProfile = () => { 271 | return new Promise((res) => setTimeout(() => { 272 | count++ 273 | res({ 274 | userId: 123, 275 | age: 20 276 | }) 277 | }, 200)) 278 | } 279 | 280 | const wrapper = mount(defineComponent({ 281 | template: '
d1:{{ data1 && data1.id }} d2:{{ data2 && data2.userId }}
', 282 | setup () { 283 | const { data: data1, error: error1 } = useSWRV('/api/user', loadUser) 284 | // TODO: checking truthiness of data1.value to avoid watcher warning 285 | // https://github.com/vuejs/composition-api/issues/242 286 | const { data: data2, error: error2 } = useSWRV(() => data1.value && '/api/profile?id=' + data1.value.id, loadProfile) 287 | return { data1, error1, data2, error2 } 288 | } 289 | })) 290 | 291 | expect(wrapper.text()).toBe('d1: d2:') 292 | timeout(100) 293 | await tick(2) 294 | expect(wrapper.text()).toBe('d1: d2:') 295 | expect(count).toEqual(0) // Promise still in flight 296 | 297 | timeout(900) 298 | await tick(2) 299 | expect(wrapper.text()).toBe('d1:123 d2:') 300 | expect(count).toEqual(1) // now that the first promise resolved, second one will fire 301 | 302 | timeout(200) 303 | await tick(2) 304 | expect(wrapper.text()).toBe('d1:123 d2:123') 305 | expect(count).toEqual(2) 306 | }) 307 | 308 | it('should not fetch if key is falsy', async () => { 309 | let count = 0 310 | const fetch = key => { 311 | count++ 312 | return new Promise(res => setTimeout(() => res(key), 100)) 313 | } 314 | const wrapper = mount(defineComponent({ 315 | template: '
{{ d1 }},{{ d2 }},{{ d3 }}
', 316 | setup () { 317 | const { data: d1 } = useSWRV('d1', fetch) 318 | const { data: d2 } = useSWRV(() => d1.value && 'd2', fetch) 319 | const { data: d3 } = useSWRV(() => d2.value && 'd3', fetch) 320 | 321 | return { d1, d2, d3 } 322 | } 323 | })) 324 | 325 | expect(count).toBe(1) 326 | expect(wrapper.text()).toBe(',,') 327 | 328 | timeout(100) 329 | await tick(2) 330 | expect(count).toBe(2) 331 | expect(wrapper.text()).toBe('d1,,') 332 | 333 | timeout(100) 334 | await tick(2) 335 | expect(count).toBe(3) 336 | expect(wrapper.text()).toBe('d1,d2,') 337 | 338 | timeout(100) 339 | await tick(3) 340 | expect(wrapper.text()).toBe('d1,d2,d3') 341 | }) 342 | 343 | it('should not revalidate if key is falsy', async () => { 344 | let count = 0 345 | const fetch = key => { 346 | count++ 347 | return new Promise(res => setTimeout(() => res(key), 100)) 348 | } 349 | const wrapper = mount(defineComponent({ 350 | template: '
{{ e1 }}
', 351 | setup () { 352 | const someDep = ref(undefined) 353 | const { data: e1 } = useSWRV(() => someDep.value, fetch, { 354 | refreshInterval: 1000 355 | }) 356 | 357 | return { e1 } 358 | } 359 | })) 360 | 361 | // Does not fetch on mount 362 | expect(count).toBe(0) 363 | expect(wrapper.text()).toBe('') 364 | timeout(100) 365 | await tick(2) 366 | expect(count).toBe(0) 367 | expect(wrapper.text()).toBe('') 368 | 369 | // Does not revalidate even after some time passes 370 | timeout(100) 371 | await tick(2) 372 | expect(count).toBe(0) 373 | expect(wrapper.text()).toBe('') 374 | 375 | // does not revalidate on refresh interval 376 | timeout(1000) 377 | await tick(2) 378 | expect(count).toBe(0) 379 | expect(wrapper.text()).toBe('') 380 | 381 | // does not revalidate on tab changes 382 | const evt = new Event('visibilitychange') 383 | document.dispatchEvent(evt) 384 | timeout(100) 385 | await tick(2) 386 | expect(count).toBe(0) 387 | expect(wrapper.text()).toBe('') 388 | }) 389 | 390 | it('should use separate configs for each invocation on the same key', async () => { 391 | const key = 'cache-key-separate-configs' 392 | let stableFetches = 0 393 | let refreshingFetches = 0 394 | const wrapper = mount(defineComponent({ 395 | template: '
stable data: {{ stableData }}, refreshing data: {{ refreshingData }}
', 396 | setup () { 397 | const { data: stableData } = useSWRV(key, () => { 398 | return ++stableFetches 399 | }, { 400 | dedupingInterval: 0, 401 | revalidateOnFocus: false 402 | }) 403 | 404 | const { data: refreshingData } = useSWRV(key, () => { 405 | return ++refreshingFetches 406 | }, { 407 | dedupingInterval: 0, 408 | revalidateOnFocus: true 409 | }) 410 | 411 | return { refreshingData, stableData } 412 | } 413 | })) 414 | 415 | await tick(2) 416 | expect(stableFetches).toBe(1) // stable defined first => fetch 417 | expect(refreshingFetches).toBe(0) // refreshing: promise is read from cache => no fetch 418 | expect(wrapper.text()).toBe('stable data: 1, refreshing data: 1') 419 | 420 | const evt = new Event('visibilitychange') 421 | document.dispatchEvent(evt) 422 | await tick(2) 423 | expect(stableFetches).toBe(1) // stable not revalidating 424 | expect(refreshingFetches).toBe(1) // refreshing is revalidating 425 | expect(wrapper.text()).toBe('stable data: 1, refreshing data: 1') 426 | 427 | document.dispatchEvent(evt) 428 | await tick(2) 429 | expect(stableFetches).toBe(1) // stable not revalidating 430 | expect(refreshingFetches).toBe(2) // refreshing is revalidating 431 | expect(wrapper.text()).toBe('stable data: 2, refreshing data: 2') 432 | }) 433 | 434 | // From #24 435 | it('should only update refs of current cache key', async () => { 436 | const fetcher = (key) => new Promise(res => setTimeout(() => res(key), 1000)) 437 | 438 | const wrapper = mount(defineComponent({ 439 | template: '
Page: {{ data }}
', 440 | setup () { 441 | const page = ref('1') 442 | const { data, error } = useSWRV(() => { 443 | return page.value 444 | }, fetcher) 445 | 446 | const interval = setInterval(() => { 447 | const nextPage: number = parseInt(page.value) + 1 448 | page.value = String(nextPage) 449 | nextPage > 2 && clearInterval(interval) 450 | }, 500) 451 | 452 | return { data, error, page } 453 | } 454 | })) 455 | const vm = wrapper.vm 456 | 457 | // initially page is empty, but fetcher has fired with page=1 458 | expect(wrapper.text()).toBe('Page:') 459 | await tick(2) 460 | // @ts-ignore 461 | expect(vm.page).toBe('1') 462 | expect(wrapper.text()).toBe('Page:') 463 | 464 | // page has now updated to page=2, fetcher1 has not yet resolved, fetcher 465 | // for page=2 has now fired 466 | timeout(500) 467 | await tick(2) 468 | // @ts-ignore 469 | expect(vm.page).toBe('2') 470 | expect(wrapper.text()).toBe('Page:') 471 | 472 | // fetcher for page=1 has resolved, but the cache key is not equal to the 473 | // current page, so the data ref does not update. fetcher for page=3 has 474 | // now fired 475 | timeout(500) 476 | await tick(2) 477 | // @ts-ignore 478 | expect(vm.page).toBe('3') 479 | expect(wrapper.text()).toBe('Page:') 480 | 481 | // cache key is no longer updating and the fetcher for page=3 has resolved 482 | // so the data ref now updates. 483 | timeout(1000) 484 | await tick(2) 485 | // @ts-ignore 486 | expect(vm.page).toBe('3') 487 | expect(wrapper.text()).toBe('Page: 3') 488 | }) 489 | 490 | it('should return cache when no fetcher provided', async () => { 491 | let invoked = 0 492 | const wrapper = mount(defineComponent({ 493 | template: '
d:{{ data }} cache:{{ dataFromCache }}
', 494 | setup () { 495 | const fetcher = () => { 496 | invoked += 1 497 | return new Promise(res => setTimeout(() => res('SWR'), 200)) 498 | } 499 | const { data } = useSWRV('cache-key-5', fetcher) 500 | const { data: dataFromCache } = useSWRV('cache-key-5') 501 | 502 | return { data, dataFromCache } 503 | } 504 | })) 505 | 506 | expect(invoked).toBe(1) 507 | 508 | expect(wrapper.text()).toBe('d: cache:') 509 | expect(invoked).toBe(1) 510 | timeout(200) 511 | await tick(2) 512 | 513 | expect(wrapper.text()).toBe('d:SWR cache:SWR') 514 | expect(invoked).toBe(1) // empty fetcher is OK 515 | }) 516 | 517 | it('should return cache when no fetcher provided, across components', async () => { 518 | let invoked = 0 519 | 520 | const Hello = (cacheKey: string) => { 521 | return defineComponent({ 522 | template: '
hello {{fromCache}}
', 523 | setup () { 524 | const { data: fromCache } = useSWRV(cacheKey) 525 | return { fromCache } 526 | } 527 | }) 528 | } 529 | 530 | const wrapper = mount(defineComponent({ 531 | template: '
data:{{ data }}
', 532 | components: { Hello: Hello('cache-key-6') }, 533 | setup () { 534 | const fetcher = () => { 535 | invoked += 1 536 | return new Promise(res => setTimeout(() => res('SWR'), 200)) 537 | } 538 | const { data } = useSWRV('cache-key-6', fetcher) 539 | 540 | return { data } 541 | } 542 | })) 543 | 544 | expect(invoked).toBe(1) 545 | 546 | expect(wrapper.text()).toBe('data:') 547 | expect(invoked).toBe(1) 548 | timeout(200) 549 | await tick(2) 550 | 551 | timeout(200) 552 | expect(wrapper.text()).toBe('data:SWR hello SWR') 553 | expect(invoked).toBe(1) // empty fetcher is OK 554 | }) 555 | 556 | it('should return data even when cache ttl expires during request', async () => { 557 | const loadData = () => new Promise(res => setTimeout(() => res('data'), 100)) 558 | let mutate 559 | const wrapper = mount(defineComponent({ 560 | template: '
hello, {{data}}, {{isValidating ? \'loading\' : \'ready\'}}
', 561 | setup () { 562 | const { data, isValidating, mutate: revalidate } = useSWRV('is-validating-3', loadData, { 563 | ttl: 50, 564 | dedupingInterval: 0 565 | }) 566 | 567 | mutate = revalidate 568 | return { 569 | data, 570 | isValidating 571 | } 572 | } 573 | })) 574 | 575 | timeout(75) 576 | await tick(2) 577 | expect(wrapper.text()).toBe('hello, , loading') 578 | 579 | timeout(25) 580 | await tick(2) 581 | expect(wrapper.text()).toBe('hello, data, ready') 582 | 583 | mutate() 584 | await tick(2) 585 | expect(wrapper.text()).toBe('hello, data, loading') 586 | timeout(25) 587 | mutate() 588 | await tick(2) 589 | expect(wrapper.text()).toBe('hello, data, loading') 590 | 591 | mutate() 592 | timeout(100) 593 | await tick(2) 594 | expect(wrapper.text()).toBe('hello, data, ready') 595 | }) 596 | 597 | // from #54 598 | it('does not invalidate cache when ttl is 0', async () => { 599 | advanceTo(new Date()) 600 | const ttl = 0 601 | let count = 0 602 | const fetch = () => { 603 | count++ 604 | return Promise.resolve(count) 605 | } 606 | 607 | mutate('ttlData1', fetch(), undefined, ttl) 608 | 609 | const wrapper1 = mount(defineComponent({ 610 | template: '
{{ data1 }}
', 611 | setup () { 612 | const { data: data1 } = useSWRV('ttlData1', undefined, { ttl, fetcher: undefined }) 613 | 614 | return { data1 } 615 | } 616 | })) 617 | const component = { 618 | template: '
{{ data2 }}
', 619 | setup () { 620 | const { data: data2 } = useSWRV('ttlData1', undefined, { ttl, fetcher: undefined }) 621 | 622 | return { data2 } 623 | } 624 | } 625 | 626 | let wrapper2 627 | await tick(2) 628 | 629 | // first time 630 | expect(count).toBe(1) 631 | expect(wrapper1.text()).toBe('1') 632 | wrapper2 = mount(defineComponent(component)) 633 | expect(wrapper2.text()).toBe('1') 634 | 635 | // after #51 gracePeriod 636 | advanceBy(6000) 637 | timeout(6000) 638 | mutate('ttlData1', fetch(), undefined, ttl) 639 | await tick(2) 640 | 641 | expect(count).toBe(2) 642 | expect(wrapper1.text()).toBe('2') 643 | wrapper2 = mount(defineComponent(component)) 644 | expect(wrapper2.text()).toBe('2') 645 | 646 | // after a long time 647 | advanceBy(100000) 648 | timeout(100000) 649 | await tick(2) 650 | 651 | expect(count).toBe(2) 652 | expect(wrapper1.text()).toBe('2') 653 | wrapper2 = mount(defineComponent(component)) 654 | expect(wrapper2.text()).toBe('2') 655 | 656 | clear() 657 | }) 658 | 659 | // from #54 660 | it('does invalidate cache when ttl is NOT 0', async () => { 661 | advanceTo(new Date()) 662 | const ttl = 100 663 | let count = 0 664 | const fetch = () => { 665 | count++ 666 | return Promise.resolve(count) 667 | } 668 | 669 | mutate('ttlData2', fetch(), undefined, ttl) 670 | 671 | const wrapper1 = mount(defineComponent({ 672 | template: '
{{ data1 }}
', 673 | setup () { 674 | const { data: data1 } = useSWRV('ttlData2', undefined, { ttl, fetcher: undefined }) 675 | 676 | return { data1 } 677 | } 678 | })) 679 | const component = { 680 | template: '
{{ data2 }}
', 681 | setup () { 682 | const { data: data2 } = useSWRV('ttlData2', undefined, { ttl, fetcher: undefined }) 683 | 684 | return { data2 } 685 | } 686 | } 687 | 688 | let wrapper2 689 | await tick(2) 690 | 691 | // first time 692 | expect(count).toBe(1) 693 | expect(wrapper1.text()).toBe('1') 694 | wrapper2 = mount(defineComponent(component)) 695 | expect(wrapper2.text()).toBe('1') 696 | 697 | // after #51 gracePeriod 698 | advanceBy(6000) 699 | timeout(6000) 700 | mutate('ttlData2', fetch(), undefined, ttl) 701 | await tick(2) 702 | 703 | expect(count).toBe(2) 704 | expect(wrapper1.text()).toBe('1') 705 | wrapper2 = mount(defineComponent(component)) 706 | expect(wrapper2.text()).toBe('2') 707 | 708 | // after a long time 709 | advanceBy(100000) 710 | timeout(100000) 711 | await tick(2) 712 | 713 | expect(count).toBe(2) 714 | expect(wrapper1.text()).toBe('1') 715 | wrapper2 = mount(defineComponent(component)) 716 | expect(wrapper2.text()).toBe('') 717 | 718 | clear() 719 | }) 720 | 721 | it('should use fetch api as default fetcher', async () => { 722 | const users = [{ name: 'bob' }, { name: 'sue' }] 723 | mockFetch(users) 724 | 725 | const wrapper = mount(defineComponent({ 726 | template: '
hello, {{ data.map(u => u.name).join(\' and \') }}
', 727 | setup () { 728 | return useSWRV('http://localhost:3000/api/users') 729 | } 730 | })) 731 | 732 | await tick(4) 733 | 734 | expect(wrapper.text()).toBe('hello, bob and sue') 735 | }) 736 | }) 737 | 738 | describe('useSWRV - loading', () => { 739 | const loadData = () => new Promise(res => setTimeout(() => res('data'), 100)) 740 | 741 | it('should return loading state via undefined data', async () => { 742 | let renderCount = 0 743 | const wrapper = mount(defineComponent({ 744 | setup () { 745 | const { data } = useSWRV('is-validating-1', loadData) 746 | return () => { 747 | renderCount++ 748 | return h('div', `hello, ${!data.value ? 'loading' : data.value}`) 749 | } 750 | } 751 | })) 752 | 753 | expect(renderCount).toEqual(1) 754 | expect(wrapper.text()).toBe('hello, loading') 755 | timeout(100) 756 | 757 | await tick(2) 758 | 759 | expect(wrapper.text()).toBe('hello, data') 760 | expect(renderCount).toEqual(2) 761 | }) 762 | 763 | it('should return loading state via isValidating', async () => { 764 | // Prime the cache 765 | const wrapper = mount(defineComponent({ 766 | setup () { 767 | const { data, isValidating } = useSWRV('is-validating-2', loadData, { 768 | refreshInterval: 1000, 769 | dedupingInterval: 0 770 | }) 771 | 772 | return () => h('div', `hello, ${data.value || ''}, ${isValidating.value ? 'loading' : 'ready'}`) 773 | } 774 | })) 775 | expect(wrapper.text()).toBe('hello, , loading') 776 | 777 | timeout(100) 778 | await tick(2) 779 | expect(wrapper.text()).toBe('hello, data, ready') 780 | 781 | // Reactive to future refreshes 782 | timeout(900) 783 | await tick(2) 784 | expect(wrapper.text()).toBe('hello, data, loading') 785 | 786 | timeout(100) 787 | await tick(2) 788 | expect(wrapper.text()).toBe('hello, data, ready') 789 | }) 790 | 791 | // #195 792 | it('should return loading state isValidating with nullish key', async () => { 793 | const wrapper = mount(defineComponent({ 794 | template: '
{{ error }}:{{this.isValidating ? \'loading\' : \'ready\'}}
', 795 | setup () { 796 | return useSWRV(() => null) 797 | } 798 | })) 799 | 800 | expect(wrapper.text()).toBe(':ready') 801 | }) 802 | 803 | it('should indicate cached data from another key with isLoading false', async () => { 804 | const key = ref(1) 805 | const wrapper = mount(defineComponent({ 806 | template: `
data: {{ String(data) }}, isValidating: {{ isValidating }}, isLoading: {{ isLoading }}
807 | ', 1373 | setup () { 1374 | function dontDoThis () { 1375 | useSWRV(() => 'error-top-level', () => 'hello') 1376 | } 1377 | 1378 | return { 1379 | dontDoThis 1380 | } 1381 | } 1382 | })) 1383 | 1384 | wrapper.find('button').trigger('click') 1385 | 1386 | expect(spy).toHaveBeenCalledWith(expect.stringContaining('Could not get current instance, check to make sure that `useSwrv` is declared in the top level of the setup function.')) 1387 | 1388 | spy.mockRestore() 1389 | }) 1390 | }) 1391 | 1392 | describe('useSWRV - window events', () => { 1393 | // @ts-ignore 1394 | const toggleVisibility = (state: VisibilityState) => Object.defineProperty(document, 'visibilityState', { 1395 | configurable: true, 1396 | // @ts-ignore 1397 | get: function (): VisibilityState { return state } 1398 | }) 1399 | 1400 | const toggleOnline = (state: boolean) => Object.defineProperty(navigator, 'onLine', { 1401 | configurable: true, 1402 | get: function (): boolean { return state } 1403 | }) 1404 | 1405 | afterEach(() => { 1406 | toggleOnline(true) 1407 | toggleVisibility('visible') 1408 | }) 1409 | 1410 | it('should not rerender when document is not visible', async () => { 1411 | let count = 0 1412 | 1413 | const wrapper = mount(defineComponent({ 1414 | template: '
count: {{ data }}
', 1415 | setup () { 1416 | return useSWRV('dynamic-5', () => count++, { 1417 | refreshInterval: 200, 1418 | dedupingInterval: 0 1419 | }) 1420 | } 1421 | })) 1422 | 1423 | await tick(1) 1424 | expect(wrapper.text()).toBe('count: 0') 1425 | 1426 | toggleVisibility(undefined) 1427 | timeout(200) 1428 | await tick(1) 1429 | // should still update even though visibilityState is undefined 1430 | expect(wrapper.text()).toBe('count: 1') 1431 | 1432 | toggleVisibility('hidden') 1433 | 1434 | timeout(200) 1435 | await tick(1) 1436 | 1437 | // should not rerender because document is hidden e.g. switched tabs 1438 | expect(wrapper.text()).toBe('count: 1') 1439 | 1440 | wrapper.unmount() 1441 | }) 1442 | 1443 | it('should get last known state when document is not visible', async () => { 1444 | let count = 0 1445 | mutate('dynamic-5-1', count) 1446 | toggleVisibility('hidden') 1447 | 1448 | const wrapper = mount(defineComponent({ 1449 | template: '
count: {{ data }}
', 1450 | setup () { 1451 | return useSWRV('dynamic-5-1', () => ++count, { 1452 | refreshInterval: 200, 1453 | dedupingInterval: 0 1454 | }) 1455 | } 1456 | })) 1457 | 1458 | // first fetch always renders #128 1459 | timeout(200) 1460 | await tick(1) 1461 | expect(wrapper.text()).toBe('count: 1') 1462 | expect(count).toBe(1) 1463 | 1464 | timeout(200) 1465 | await tick(1) 1466 | expect(wrapper.text()).toBe('count: 1') 1467 | expect(count).toBe(1) 1468 | 1469 | timeout(200) 1470 | await tick(1) 1471 | expect(wrapper.text()).toBe('count: 1') 1472 | expect(count).toBe(1) 1473 | 1474 | timeout(200) 1475 | await tick(1) 1476 | expect(wrapper.text()).toBe('count: 1') 1477 | expect(count).toBe(1) 1478 | 1479 | // subsequent fetches while document is hidden do not rerender 1480 | timeout(200) 1481 | await tick(1) 1482 | expect(wrapper.text()).toBe('count: 1') 1483 | expect(count).toBe(1) 1484 | 1485 | toggleVisibility('visible') 1486 | 1487 | timeout(200) 1488 | await tick(1) 1489 | expect(wrapper.text()).toBe('count: 2') 1490 | expect(count).toBe(2) 1491 | 1492 | timeout(200) 1493 | await tick(1) 1494 | expect(wrapper.text()).toBe('count: 2') 1495 | expect(count).toBe(2) 1496 | 1497 | toggleVisibility('visible') 1498 | 1499 | timeout(200) 1500 | await tick(1) 1501 | expect(wrapper.text()).toBe('count: 3') 1502 | expect(count).toBe(3) 1503 | 1504 | timeout(200) 1505 | await tick(1) 1506 | expect(wrapper.text()).toBe('count: 3') 1507 | expect(count).toBe(3) 1508 | 1509 | timeout(200) 1510 | await tick(1) 1511 | expect(wrapper.text()).toBe('count: 4') 1512 | expect(count).toBe(4) 1513 | 1514 | timeout(200) 1515 | await tick(1) 1516 | expect(wrapper.text()).toBe('count: 4') 1517 | expect(count).toBe(4) 1518 | 1519 | wrapper.unmount() 1520 | }) 1521 | 1522 | it('should not rerender when offline', async () => { 1523 | let count = 0 1524 | 1525 | const wrapper = mount(defineComponent({ 1526 | template: '
count: {{ data }}
', 1527 | setup () { 1528 | return useSWRV('dynamic-6', () => count++, { 1529 | refreshInterval: 200, 1530 | dedupingInterval: 0 1531 | }) 1532 | } 1533 | })) 1534 | 1535 | await tick(1) 1536 | expect(wrapper.text()).toBe('count: 0') 1537 | 1538 | toggleOnline(undefined) 1539 | 1540 | timeout(200) 1541 | await tick(1) 1542 | // should rerender since we're AMERICA ONLINE 1543 | expect(wrapper.text()).toBe('count: 1') 1544 | 1545 | // connection drops... your mom picked up the phone while you were 🏄‍♂️ the 🕸 1546 | toggleOnline(false) 1547 | 1548 | timeout(200) 1549 | await tick(1) 1550 | // should not rerender cuz offline 1551 | expect(wrapper.text()).toBe('count: 1') 1552 | }) 1553 | 1554 | // https://github.com/Kong/swrv/issues/128 1555 | it('fetches data on first render even when document is not visible', async () => { 1556 | toggleVisibility('hidden') 1557 | 1558 | const wrapper = mount(defineComponent({ 1559 | template: '
{{ data }}
', 1560 | setup () { 1561 | const { data, error } = useSWRV( 1562 | 'fetches-data-even-when-document-is-not-visible', 1563 | () => new Promise(res => setTimeout(() => res('first'), 100)) 1564 | ) 1565 | return { data, error } 1566 | } 1567 | })) 1568 | 1569 | expect(wrapper.text()).toBe('') 1570 | 1571 | timeout(100) 1572 | await tick() 1573 | 1574 | expect(wrapper.text()).toBe('first') 1575 | }) 1576 | }) 1577 | 1578 | describe('useSWRV - ref cache management', () => { 1579 | beforeEach(() => { 1580 | // Isolate the changes to the caches made by the tests in this block. 1581 | if (mockDataCache) { 1582 | mockDataCache.items = new Map() 1583 | } 1584 | if (mockRefCache) { 1585 | mockRefCache.items = new Map() 1586 | } 1587 | if (mockPromisesCache) { 1588 | mockPromisesCache.items = new Map() 1589 | } 1590 | }) 1591 | it('useSwrv should remove stateRef from ref cache when the component is unmounted', async () => { 1592 | const key = 'key' 1593 | const fetchedValue = 'SWR' 1594 | const fetch = () => fetchedValue 1595 | const vm = mount(defineComponent({ 1596 | template: '
', 1597 | setup () { 1598 | return useSWRV(key, fetch) 1599 | } 1600 | })) 1601 | expect(mockRefCache.get(key).data).toHaveLength(1) 1602 | vm.unmount() 1603 | expect(mockRefCache.get(key).data).toHaveLength(0) 1604 | }) 1605 | 1606 | it('useSwrv should keep stateRefs from other components when its component is unmounted', async () => { 1607 | const key = 'key' 1608 | const fetchedValue = 'SWR' 1609 | const fetch = () => fetchedValue 1610 | const originalVm = mount(defineComponent({ 1611 | template: '
', 1612 | setup () { 1613 | return useSWRV(key, fetch) 1614 | } 1615 | })) 1616 | 1617 | expect(mockRefCache.get(key).data).toHaveLength(1) 1618 | // Create another Vue component that calls useSwrv with the same key. 1619 | mount(defineComponent({ 1620 | template: '
', 1621 | setup () { 1622 | return useSWRV(key, fetch) 1623 | } 1624 | })) 1625 | 1626 | expect(mockRefCache.get(key).data).toHaveLength(2) 1627 | originalVm.unmount() 1628 | expect(mockRefCache.get(key).data).toHaveLength(1) 1629 | }) 1630 | }) 1631 | -------------------------------------------------------------------------------- /tests/utils/jest-timeout.ts: -------------------------------------------------------------------------------- 1 | export default function timeout (milliseconds: number) { 2 | jest.advanceTimersByTime(milliseconds) 3 | } 4 | 5 | -------------------------------------------------------------------------------- /tests/utils/tick.ts: -------------------------------------------------------------------------------- 1 | import { nextTick } from 'vue' 2 | 3 | export default async function tick (times = 1) { 4 | for (const _ in [...Array(times).keys()]) { 5 | await nextTick() 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "include": [ 4 | "src/**/*.ts", 5 | "src/**/*.tsx", 6 | "src/**/*.vue" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "declaration": true, 6 | "downlevelIteration": true, 7 | "jsx": "preserve", 8 | "moduleResolution": "node", 9 | "noEmitOnError": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "noImplicitReturns": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "esModuleInterop": true, 15 | "allowSyntheticDefaultImports": true, 16 | "sourceMap": true, 17 | "baseUrl": ".", 18 | "outDir": "./dist", 19 | "types": [ 20 | "webpack-env", 21 | "node", "jest" 22 | ], 23 | "lib": [ 24 | "esnext", 25 | "dom", 26 | "dom.iterable", 27 | "scripthost" 28 | ], 29 | "typeRoots": [ 30 | "./types", 31 | "./node_modules/@types" 32 | ], 33 | "skipLibCheck": true 34 | }, 35 | "include": [ 36 | "src/**/*.ts", 37 | "src/**/*.tsx", 38 | "src/**/*.vue", 39 | "tests/**/*.ts", 40 | "tests/**/*.tsx" 41 | ], 42 | "exclude": [ 43 | "node_modules" 44 | ] 45 | } 46 | --------------------------------------------------------------------------------