├── .browserslistrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── README.md ├── babel.config.js ├── package.json ├── postcss.config.js ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── assets │ └── tailwind.css ├── components │ ├── UserAvatar.vue │ └── UserProfile.vue ├── composables │ └── swr-cache.js ├── main.js └── utils │ ├── as-array.js │ └── fetcher.js ├── tailwind.config.js ├── vue.config.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | 4 | !.eslintrc.js 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: [ 7 | `plugin:vue/vue3-essential`, 8 | `@vue/airbnb`, 9 | `@avalanche/eslint-config`, 10 | ], 11 | rules: { 12 | 'no-console': process.env.NODE_ENV === `production` ? `warn` : `off`, 13 | 'no-debugger': process.env.NODE_ENV === `production` ? `warn` : `off`, 14 | }, 15 | parserOptions: { 16 | ecmaVersion: 2020, 17 | parser: `babel-eslint`, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Numerous always-ignore extensions 2 | *.diff 3 | *.err 4 | *.log 5 | *.orig 6 | *.rej 7 | *.swo 8 | *.swp 9 | *.tgz 10 | *.vi 11 | *.zip 12 | *~ 13 | 14 | # OS or Editor folders 15 | ._* 16 | .cache 17 | .DS_Store 18 | .idea 19 | .project 20 | .settings 21 | .tmproj 22 | *.esproj 23 | *.sublime-project 24 | *.sublime-workspace 25 | nbproject 26 | Thumbs.db 27 | 28 | # Folders to ignore 29 | dist 30 | node_modules 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-3-composition-api-data-fetching-composable 2 | 3 | [![Patreon](https://img.shields.io/badge/patreon-donate-blue.svg)](https://www.patreon.com/maoberlehner) 4 | [![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://paypal.me/maoberlehner) 5 | 6 | ## Build Setup 7 | 8 | ```bash 9 | # Install dependencies. 10 | yarn install 11 | 12 | # Serve with hot reload. 13 | yarn serve 14 | 15 | # Build for production with minification. 16 | yarn build 17 | 18 | # Lint all files. 19 | yarn lint 20 | ``` 21 | 22 | ## About 23 | 24 | ### Author 25 | 26 | Markus Oberlehner 27 | Website: https://markus.oberlehner.net 28 | Twitter: https://twitter.com/MaOberlehner 29 | PayPal.me: https://paypal.me/maoberlehner 30 | Patreon: https://www.patreon.com/maoberlehner 31 | 32 | ### License 33 | 34 | MIT 35 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | `@vue/cli-plugin-babel/preset`, 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-3-composition-api-data-fetching-composable", 3 | "version": "0.1.0", 4 | "author": "Markus Oberlehner", 5 | "homepage": "https://github.com/maoberlehner/vue-3-composition-api-data-fetching-composable", 6 | "license": "MIT", 7 | "private": true, 8 | "scripts": { 9 | "serve": "vue-cli-service serve", 10 | "build": "vue-cli-service build", 11 | "lint": "vue-cli-service lint" 12 | }, 13 | "dependencies": { 14 | "core-js": "^3.6.5", 15 | "lru-cache": "^5.1.1", 16 | "vue": "^3.0.0-beta.1" 17 | }, 18 | "devDependencies": { 19 | "@avalanche/eslint-config": "^4.0.0", 20 | "@vue/cli-plugin-babel": "~4.3.1", 21 | "@vue/cli-plugin-eslint": "~4.3.1", 22 | "@vue/cli-service": "~4.3.1", 23 | "@vue/compiler-sfc": "^3.0.0-beta.1", 24 | "@vue/eslint-config-airbnb": "^5.0.2", 25 | "babel-eslint": "^10.1.0", 26 | "eslint": "^6.8.0", 27 | "eslint-plugin-import": "^2.20.2", 28 | "eslint-plugin-vue": "^7.0.0-alpha.0", 29 | "vue-cli-plugin-tailwind": "~1.4.1", 30 | "vue-cli-plugin-vue-next": "~0.1.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | 'vue-cli-plugin-tailwind/purgecss': {}, 5 | autoprefixer: {}, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maoberlehner/vue-3-composition-api-data-fetching-composable/1a44370a9be58276baa1ad145029a5bba0d5ed74/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 54 | -------------------------------------------------------------------------------- /src/assets/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | @tailwind components; 4 | 5 | @tailwind utilities; 6 | -------------------------------------------------------------------------------- /src/components/UserAvatar.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 50 | -------------------------------------------------------------------------------- /src/components/UserProfile.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 83 | -------------------------------------------------------------------------------- /src/composables/swr-cache.js: -------------------------------------------------------------------------------- 1 | import { reactive, readonly, toRefs } from 'vue'; 2 | import LRU from 'lru-cache'; 3 | 4 | import { asArray } from '../utils/as-array'; 5 | 6 | const CACHE = new LRU({ max: 1024 }); 7 | 8 | /** 9 | * @typedef {Object} SwrCacheOptions 10 | * @property {number} dedupingInterval Dedupe calls with the same signature in this time span in ms. 11 | */ 12 | 13 | /** 14 | * @typedef {Object} SwrCacheResponse 15 | * @property {any} data The (resolved) return value of the callback. 16 | * @property {Error} error Error thrown by the callback. 17 | * @property {Function} reload Function to manually call the callback function again. 18 | * @property {Symbol} state Current state in the lifecycle. 19 | */ 20 | 21 | /** 22 | * @type SwrCacheOptions 23 | */ 24 | const DEFAULT_OPTIONS = { 25 | dedupingInterval: 2000, 26 | }; 27 | 28 | export const STATE = { 29 | error: Symbol(`error`), 30 | idle: Symbol(`idle`), 31 | loading: Symbol(`loading`), 32 | revalidating: Symbol(`revalidating`), 33 | }; 34 | 35 | /** 36 | * A composable for caching expensive work (like data fetching) using the 37 | * stale-while-revalidate invalidation strategy. 38 | * @param {any} parameter A single parameter or an array of parameters to be passed to the callback. 39 | * @param {Function} callback A callback function which typically returns a promise. 40 | * @param {SwrCacheOptions} [customOptions] Custom configuration options. 41 | */ 42 | export function useSwrCache(parameter, callback, customOptions) { 43 | /** 44 | * @type SwrCacheOptions 45 | */ 46 | const options = { 47 | ...DEFAULT_OPTIONS, 48 | ...customOptions, 49 | }; 50 | 51 | const parameters = asArray(parameter); 52 | const cacheKey = `${JSON.stringify(parameters)}${callback.toString()}`; 53 | const cacheKeyDedupe = `${cacheKey}_dedupe`; 54 | const cachedResponse = CACHE.get(cacheKey); 55 | 56 | /** 57 | * @type SwrCacheResponse 58 | */ 59 | const response = cachedResponse || reactive({ 60 | data: null, 61 | error: null, 62 | reload: undefined, 63 | state: undefined, 64 | }); 65 | 66 | if (!cachedResponse) CACHE.set(cacheKey, response); 67 | 68 | const load = async () => { 69 | try { 70 | if (CACHE.get(cacheKeyDedupe)) return; 71 | 72 | CACHE.set(cacheKeyDedupe, true, options.dedupingInterval); 73 | 74 | response.state = response.data ? STATE.revalidating : STATE.loading; 75 | response.data = Object.freeze(await callback(...parameters)); 76 | response.state = STATE.idle; 77 | } catch (error) { 78 | console.error(error); 79 | 80 | CACHE.del(cacheKeyDedupe); 81 | 82 | response.error = error; 83 | response.state = STATE.error; 84 | } 85 | }; 86 | 87 | response.reload = load; 88 | load(); 89 | 90 | return toRefs(readonly(response)); 91 | } 92 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | 3 | import './assets/tailwind.css'; 4 | 5 | import App from './App.vue'; 6 | 7 | createApp(App).mount(`#app`); 8 | -------------------------------------------------------------------------------- /src/utils/as-array.js: -------------------------------------------------------------------------------- 1 | export const asArray = x => [].concat(x); 2 | -------------------------------------------------------------------------------- /src/utils/fetcher.js: -------------------------------------------------------------------------------- 1 | let count = 0; 2 | 3 | const responses = [ 4 | { 5 | company: { name: `Microsoft` }, 6 | email: `john@microsoft.com`, 7 | name: `John Smith`, 8 | website: `https://microsoft.com/john`, 9 | }, 10 | { 11 | company: { name: `Apple` }, 12 | email: `john@apple.com`, 13 | name: `John Smith`, 14 | website: `https://apple.com/john`, 15 | }, 16 | { 17 | company: { name: `Amazon` }, 18 | email: `john@amazon.com`, 19 | name: `John Smith`, 20 | website: `https://amazon.com/john`, 21 | }, 22 | ]; 23 | 24 | export const fetcher = endpoint => new Promise((resolve) => { 25 | console.log(`Fetch: ${endpoint}`); 26 | setTimeout(() => { 27 | console.log(`Respond:`, responses[count]); 28 | resolve(responses[count]); 29 | if (count === 2) { 30 | count = 0; 31 | return; 32 | } 33 | count += 1; 34 | }, 2000); 35 | }); 36 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: [], 3 | theme: { 4 | extend: {}, 5 | }, 6 | variants: {}, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | lintOnSave: false, 3 | }; 4 | --------------------------------------------------------------------------------