├── .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 | [](https://www.patreon.com/maoberlehner)
4 | [](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 |
2 |
6 |
7 |
12 |
13 |
19 |
27 |
28 |
29 |
30 |
31 |
54 |
--------------------------------------------------------------------------------
/src/assets/tailwind.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 |
3 | @tailwind components;
4 |
5 | @tailwind utilities;
6 |
--------------------------------------------------------------------------------
/src/components/UserAvatar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |

13 |
14 |
15 |
{{ user.name }}
16 |
{{ user.email }}
17 |
18 |
19 |
20 | LOADING ...
21 |
22 |
23 | {{ error }}
24 |
25 |
26 |
27 |
28 |
50 |
--------------------------------------------------------------------------------
/src/components/UserProfile.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 | Profile
10 |
11 |
12 | Personal details.
13 |
14 |
15 |
16 |
17 |
18 |
-
19 | Full name
20 |
21 | -
22 | {{ user.name }}
23 |
24 |
25 |
26 |
-
27 | Email
28 |
29 | -
30 | {{ user.email }}
31 |
32 |
33 |
34 |
-
35 | Website
36 |
37 | -
38 | {{ user.website }}
39 |
40 |
41 |
42 |
-
43 | Company
44 |
45 | -
46 | {{ user.company.name }}
47 |
48 |
49 |
50 |
51 |
52 |
53 | LOADING ...
54 |
55 |
56 | {{ error }}
57 |
58 |
59 |
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 |
--------------------------------------------------------------------------------