├── .npmrc
├── lib
├── cache
│ ├── index.ts
│ └── adapters
│ │ └── map.ts
├── vite-env.d.ts
├── config
│ ├── enviroment.ts
│ ├── index.ts
│ ├── global-state.ts
│ ├── injection-keys.ts
│ └── swr-config.ts
├── index.ts
├── composables
│ ├── index.ts
│ ├── scope-state
│ │ ├── index.ts
│ │ └── scope-state.spec.ts
│ ├── swr
│ │ ├── swr-ssr.spec.ts
│ │ ├── swr-cache.spec.ts
│ │ ├── swr-types.test-d.ts
│ │ ├── swr-fetcher.spec.ts
│ │ ├── swr-deduping.spec.ts
│ │ ├── swr-mutate.spec.ts
│ │ ├── swr-callbacks.spec.ts
│ │ ├── swr-revalidate.spec.ts
│ │ ├── index.ts
│ │ ├── swr-stale.spec.ts
│ │ └── swr.spec.ts
│ └── global-swr-config
│ │ ├── index.ts
│ │ └── global-swr-config.spec.ts
├── types
│ ├── index.ts
│ ├── utils.ts
│ ├── generics.ts
│ └── lib.ts
├── utils
│ ├── type-assertions
│ │ └── index.ts
│ ├── index.ts
│ ├── chain-fn
│ │ ├── index.ts
│ │ └── chain-fn.spec.ts
│ ├── check-types
│ │ ├── index.ts
│ │ └── check-types.spec.ts
│ ├── merge-config
│ │ ├── index.ts
│ │ └── merge-config.spec.ts
│ ├── subscribe-key
│ │ ├── index.ts
│ │ └── subscribe-key.spec.ts
│ ├── serialize-key
│ │ ├── index.ts
│ │ └── serialize-key.spec.ts
│ ├── with-setup
│ │ └── index.ts
│ ├── test
│ │ ├── test.spec.ts
│ │ └── index.ts
│ └── stable-hash
│ │ ├── index.ts
│ │ └── stable-hash.spec.ts
└── devtools.ts
├── .husky
├── pre-commit
└── commit-msg
├── tsconfig.eslint.json
├── commitlint.config.js
├── .eslintrc.json
├── .releaserc.json
├── .prettierrc.json
├── .gitignore
├── docs
├── typescript.md
├── index.md
├── introduction.md
├── data-fetching.md
├── global-configuration.md
├── conditional-fetching.md
├── revalidation.md
├── .vitepress
│ └── config.js
├── options.md
├── getting-started.md
├── error-handling.md
└── mutation.md
├── .github
└── workflows
│ ├── pull-request.yml
│ ├── deploy-docs.yml
│ └── release.yml
├── tsconfig.json
├── vite.config.ts
├── LICENSE
├── README.md
├── vitest.setup.ts
├── CONTRIBUTING.md
└── package.json
/.npmrc:
--------------------------------------------------------------------------------
1 | shamefully-hoist=true
2 |
--------------------------------------------------------------------------------
/lib/cache/index.ts:
--------------------------------------------------------------------------------
1 | export * from './adapters/map';
2 |
--------------------------------------------------------------------------------
/lib/cache/adapters/map.ts:
--------------------------------------------------------------------------------
1 | export const MapAdapter = Map;
2 |
--------------------------------------------------------------------------------
/lib/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/lib/config/enviroment.ts:
--------------------------------------------------------------------------------
1 | export { isClient } from '@vueuse/core';
2 |
--------------------------------------------------------------------------------
/lib/index.ts:
--------------------------------------------------------------------------------
1 | export * from './composables';
2 | export * from './devtools';
3 |
--------------------------------------------------------------------------------
/lib/composables/index.ts:
--------------------------------------------------------------------------------
1 | export * from './swr';
2 | export * from './global-swr-config';
3 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | pnpm exec lint-staged
5 |
--------------------------------------------------------------------------------
/lib/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './generics';
2 | export * from './lib';
3 | export * from './utils';
4 |
--------------------------------------------------------------------------------
/lib/types/utils.ts:
--------------------------------------------------------------------------------
1 | export type OmitFirstArrayIndex = T extends [any, ...infer Rest] ? Rest : never;
2 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | pnpm exec commitlint --edit "${1}"
5 |
--------------------------------------------------------------------------------
/lib/utils/type-assertions/index.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line import/no-extraneous-dependencies
2 | export * from 'tsd';
3 |
--------------------------------------------------------------------------------
/tsconfig.eslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": ["lib", "vite.config.ts", "commitlint.config.js", "vitest.setup.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@commitlint/config-conventional'],
3 | ignores: [(message) => message.includes('[skip ci]')],
4 | };
5 |
--------------------------------------------------------------------------------
/lib/config/index.ts:
--------------------------------------------------------------------------------
1 | export * from './swr-config';
2 | export * from './injection-keys';
3 | export * from './enviroment';
4 | export * from './global-state';
5 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "extends": "@edumudu/eslint-config/vue-ts",
4 | "parserOptions": {
5 | "project": "./tsconfig.eslint.json"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/lib/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './check-types';
2 | export * from './stable-hash';
3 | export * from './serialize-key';
4 | export * from './merge-config';
5 | export * from './chain-fn';
6 | export * from './subscribe-key';
7 |
--------------------------------------------------------------------------------
/lib/types/generics.ts:
--------------------------------------------------------------------------------
1 | import { Ref } from 'vue';
2 |
3 | export type AnyFunction = (...args: any[]) => any | Promise;
4 |
5 | export type MaybeRef = T | Ref;
6 | export type DeepMaybeRef = { [K in keyof T]: MaybeRef };
7 |
--------------------------------------------------------------------------------
/.releaserc.json:
--------------------------------------------------------------------------------
1 | {
2 | "branches": ["main"],
3 | "plugins": [
4 | "@semantic-release/commit-analyzer",
5 | "@semantic-release/release-notes-generator",
6 | "@semantic-release/npm",
7 | "@semantic-release/github",
8 | "@semantic-release/git"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/lib/config/global-state.ts:
--------------------------------------------------------------------------------
1 | import { reactive } from 'vue';
2 |
3 | import { CacheProvider, CacheState, ScopeState } from '@/types';
4 |
5 | /**
6 | * Holds the scope's states, isolated using cacheProvider as key
7 | */
8 | export const globalState = reactive(new WeakMap, ScopeState>());
9 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "arrowParens": "always",
4 | "singleQuote": true,
5 | "trailingComma": "all",
6 | "bracketSpacing": true,
7 | "semi": true,
8 | "tabWidth": 2,
9 | "bracketSameLine": false,
10 | "vueIndentScriptAndStyle": false,
11 | "singleAttributePerLine": true
12 | }
13 |
--------------------------------------------------------------------------------
/lib/config/injection-keys.ts:
--------------------------------------------------------------------------------
1 | import { DeepReadonly, InjectionKey, Ref } from 'vue';
2 |
3 | import { SWRConfig } from '@/types';
4 |
5 | /**
6 | * Key for provide and get current context configs
7 | * @internal
8 | */
9 | export const globalConfigKey = Symbol('SWR global config key') as InjectionKey<
10 | DeepReadonly>
11 | >;
12 |
--------------------------------------------------------------------------------
/lib/utils/chain-fn/index.ts:
--------------------------------------------------------------------------------
1 | import { AnyFunction } from '@/types';
2 |
3 | export const chainFns = (...fns: F) => {
4 | const validFns = fns.filter((maybeFn: T | undefined): maybeFn is T => !!maybeFn);
5 |
6 | return (...params: Parameters>) =>
7 | validFns.forEach((fn) => fn(...params));
8 | };
9 |
--------------------------------------------------------------------------------
/lib/utils/check-types/index.ts:
--------------------------------------------------------------------------------
1 | import type { AnyFunction } from '@/types';
2 |
3 | export const isFunction = (value: unknown): value is T =>
4 | typeof value === 'function';
5 |
6 | export const isDate = (date: any): date is T => date.constructor === Date;
7 |
8 | export const isUndefined = (value: T | undefined): value is undefined =>
9 | typeof value === 'undefined';
10 |
11 | export { isObject } from '@vueuse/core';
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | .eslintcache
27 |
28 | # test
29 | coverage
30 |
31 | # docs
32 | docs/.vitepress/dist/*
33 | docs/.vitepress/cache/*
34 |
--------------------------------------------------------------------------------
/lib/config/swr-config.ts:
--------------------------------------------------------------------------------
1 | import { reactive } from 'vue';
2 |
3 | import { SWRConfig } from '@/types';
4 | import { MapAdapter } from '@/cache';
5 |
6 | export const defaultConfig: SWRConfig = {
7 | cacheProvider: reactive(new MapAdapter()),
8 | revalidateOnFocus: true,
9 | revalidateOnReconnect: true,
10 | revalidateIfStale: true,
11 | focusThrottleInterval: 5000,
12 | dedupingInterval: 2000,
13 | refreshInterval: 0,
14 | refreshWhenHidden: false,
15 | refreshWhenOffline: false,
16 | };
17 |
--------------------------------------------------------------------------------
/docs/typescript.md:
--------------------------------------------------------------------------------
1 | # Typescript
2 |
3 | `swr-vue` is written in typescript and is type safe out of the box.
4 |
5 | ## Generics
6 |
7 | To specify the type of `data` by default, it will use the return type of `fetcher` (with `undefined` for the non-ready state) as the data type, but you can also pass it as a parameter:
8 |
9 | ```ts
10 | // Specify the data type:
11 | // `fetchUser` is `(endpoint: string) => User`.
12 | const { data } = useSWR('/api/user', fetchUser)
13 |
14 | // Specify the error type:
15 | // `fetchUser` is `(endpoint: string) => User`.
16 | const { data } = useSWR('/api/user', fetchUser)
17 | ```
18 |
--------------------------------------------------------------------------------
/lib/utils/merge-config/index.ts:
--------------------------------------------------------------------------------
1 | import { SWRConfig } from '@/types';
2 |
3 | import { chainFns } from '../chain-fn';
4 |
5 | export const mergeConfig = , D extends Partial>(
6 | fallbackConfig: T,
7 | config: D,
8 | ) => {
9 | const onSuccess = [config.onSuccess, fallbackConfig.onSuccess].filter(Boolean);
10 | const onError = [config.onError, fallbackConfig.onError].filter(Boolean);
11 |
12 | return {
13 | ...fallbackConfig,
14 | ...config,
15 |
16 | onSuccess: onSuccess.length > 0 ? chainFns(...onSuccess) : undefined,
17 | onError: onError.length > 0 ? chainFns(...onError) : undefined,
18 | };
19 | };
20 |
--------------------------------------------------------------------------------
/lib/utils/subscribe-key/index.ts:
--------------------------------------------------------------------------------
1 | export type SubscribeCallback = () => void;
2 | export type SubscribeCallbackCache = Map;
3 |
4 | export const unsubscribeCallback = (
5 | key: string,
6 | cb: SubscribeCallback,
7 | cbCache: SubscribeCallbackCache,
8 | ) => {
9 | const callbacks = cbCache.get(key) || [];
10 | const newCallbacks = callbacks.filter((currentCb) => currentCb !== cb);
11 |
12 | cbCache.set(key, newCallbacks);
13 | };
14 |
15 | export const subscribeCallback = (
16 | key: string,
17 | cb: SubscribeCallback,
18 | cbCache: SubscribeCallbackCache,
19 | ) => {
20 | const callbacks = cbCache.get(key) || [];
21 |
22 | cbCache.set(key, [...callbacks, cb]);
23 |
24 | return () => unsubscribeCallback(key, cb, cbCache);
25 | };
26 |
--------------------------------------------------------------------------------
/lib/utils/serialize-key/index.ts:
--------------------------------------------------------------------------------
1 | import { createUnrefFn } from '@vueuse/core';
2 |
3 | import type { Key, KeyArguments } from '@/types';
4 |
5 | import { isFunction } from '../check-types';
6 | import { stableHash } from '../stable-hash';
7 |
8 | export const serializeKey = createUnrefFn((key: Key) => {
9 | let sanitizedKey: KeyArguments = key;
10 |
11 | if (isFunction(sanitizedKey)) {
12 | try {
13 | sanitizedKey = sanitizedKey();
14 | } catch {
15 | sanitizedKey = '';
16 | }
17 | }
18 |
19 | const isEmptyArray = Array.isArray(sanitizedKey) && sanitizedKey.length === 0;
20 |
21 | return {
22 | key: !isEmptyArray && !!sanitizedKey ? stableHash(sanitizedKey) : '',
23 | args: Array.isArray(sanitizedKey) ? sanitizedKey : [sanitizedKey],
24 | };
25 | });
26 |
--------------------------------------------------------------------------------
/lib/utils/chain-fn/chain-fn.spec.ts:
--------------------------------------------------------------------------------
1 | import { chainFns } from '.';
2 |
3 | describe('chainFn', () => {
4 | it('should call all cahined functions', () => {
5 | const functions = [vi.fn(), vi.fn(), vi.fn(), vi.fn(), vi.fn(), vi.fn(), vi.fn(), vi.fn()];
6 | const chainedFn = chainFns(...functions);
7 |
8 | chainedFn();
9 |
10 | functions.forEach((fn) => expect(fn).toHaveBeenCalledOnce());
11 | });
12 |
13 | it('should call all chained functions with the same arguments', () => {
14 | const functions = [vi.fn(), vi.fn(), vi.fn(), vi.fn(), vi.fn(), vi.fn(), vi.fn(), vi.fn()];
15 | const chainedFn = chainFns(...functions);
16 |
17 | chainedFn('arg1', 'arg2', 3);
18 |
19 | functions.forEach((fn) => expect(fn).toHaveBeenCalledWith('arg1', 'arg2', 3));
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/lib/utils/with-setup/index.ts:
--------------------------------------------------------------------------------
1 | import { App, createApp } from 'vue';
2 |
3 | /**
4 | * Use this function for test coposables that depend on lifecycles or provide/inject
5 | */
6 | export function withSetup) => any>(
7 | composable: C,
8 | ): [ReturnType, App] {
9 | let result: ReturnType = null as any;
10 |
11 | const app = createApp({
12 | name: 'TestApp',
13 | setup() {
14 | result = composable(app);
15 |
16 | // suppress missing template warning
17 | // eslint-disable-next-line @typescript-eslint/no-empty-function
18 | return () => {};
19 | },
20 | });
21 |
22 | app.mount(document.createElement('div'));
23 | // return the result and the app instance
24 | // for testing provide / unmount
25 | return [result, app];
26 | }
27 |
--------------------------------------------------------------------------------
/.github/workflows/pull-request.yml:
--------------------------------------------------------------------------------
1 | name: PR-Checks
2 |
3 | on: [pull_request]
4 |
5 | jobs:
6 | verify_pr:
7 | name: "Verify PR"
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - uses: actions/checkout@v3
12 | - uses: pnpm/action-setup@v2
13 |
14 | - name: Setup Node.js
15 | uses: actions/setup-node@v3
16 | with:
17 | node-version: '16'
18 | cache: 'pnpm'
19 | cache-dependency-path: '**/pnpm-lock.yaml'
20 |
21 | - name: Install dependencies
22 | run: pnpm i --frozen-lockfile
23 |
24 | - name: Check lint
25 | run: pnpm run lint:js
26 |
27 | - name: Check Tests
28 | run: pnpm run test
29 |
30 | - name: Build lib
31 | run: pnpm run build
32 |
33 | - name: Check Types
34 | run: pnpm run test:types
35 |
--------------------------------------------------------------------------------
/lib/utils/test/test.spec.ts:
--------------------------------------------------------------------------------
1 | import { dispatchEvent } from '.';
2 |
3 | describe('test utils', () => {
4 | describe('dispatchEvent', () => {
5 | it.each([
6 | ['click', document.createElement('div')],
7 | ['click', window],
8 | ['focus', window],
9 | ['blur', window],
10 | ['click', document],
11 | ['focus', document],
12 | ['blur', document],
13 | ['click', document.body],
14 | ['click', document.createElement('span')],
15 | ['click', document.createElement('section')],
16 | ])('should dispatch event "%s" in the passed DOM element %s', (eventName, el) => {
17 | const onEvent = vi.fn();
18 |
19 | el.addEventListener(eventName, onEvent);
20 | dispatchEvent(eventName, el);
21 |
22 | expect(onEvent).toHaveBeenCalledOnce();
23 | });
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | # https://vitepress.dev/reference/default-theme-home-page
3 | layout: home
4 |
5 | hero:
6 | name: SWR Vue
7 | text: Vue Stale While Revalidate
8 | tagline: Docs of SWR Vue
9 | actions:
10 | - theme: brand
11 | text: Get Started
12 | link: /getting-started
13 | - theme: alt
14 | text: API Examples
15 | link: /options
16 | - theme: alt
17 | text: View on Github
18 | link: https://github.com/edumudu/swr-vue
19 |
20 | features:
21 | - title: Looking for help?
22 | details: In case you need help, you can start a discussion in the GitHub Discussions.
23 | link: https://github.com/edumudu/swr-vue/discussions
24 | linkText: Start a discussion
25 | icon: 📝
26 | - title: Type Strong
27 | details: Written in TypeScript, with full TS docs
28 | icon: 🦾
29 | ---
30 |
--------------------------------------------------------------------------------
/docs/introduction.md:
--------------------------------------------------------------------------------
1 | # SWR Vue
2 |
3 | > This project is inspired by the awesome work [vercel/swr](https://swr.vercel.app/pt-BR)
4 |
5 | SWR derives from `stale-while-revalidate`, a HTTP cache technique. This technique consists in first return the data cached(stale), after send a request(revalidate) and finaly return the newly data.
6 |
7 | > You can read more about this [here](https://web.dev/stale-while-revalidate/)
8 |
9 | ## Features
10 |
11 | - Fast and reusable data fetching
12 | - Typescript friendly
13 | - Protocol agnostic
14 | - Request lib agnostic
15 | - Integrated cache
16 | - Optimistic UI
17 | - Request deduplication
18 | - Revalidation on focus and network recovery
19 | - Polling
20 | - Configurable
21 |
22 | ## Comunity
23 |
24 | Feel free to join us to contribute or ask doubts in [Github discussions](https://github.com/edumudu/swr-vue/discussions)
25 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-docs.yml:
--------------------------------------------------------------------------------
1 | name: Deploy Docs
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | - '**/docs/**'
9 |
10 | jobs:
11 | deploy:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 | - uses: pnpm/action-setup@v2
16 |
17 | - name: Setup Node.js
18 | uses: actions/setup-node@v3
19 | with:
20 | node-version: '16'
21 | cache: 'pnpm'
22 | cache-dependency-path: '**/pnpm-lock.yaml'
23 |
24 | - name: Install dependencies
25 | run: pnpm i --frozen-lockfile
26 |
27 | - name: Build
28 | run: pnpm docs:build
29 |
30 | - name: Deploy
31 | uses: peaceiris/actions-gh-pages@v3
32 | with:
33 | github_token: ${{ secrets.GITHUB_TOKEN }}
34 | publish_dir: docs/.vitepress/dist
35 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "lib": ["ESNext", "DOM"],
7 | "moduleResolution": "Node",
8 | "strict": true,
9 | "sourceMap": true,
10 | "resolveJsonModule": true,
11 | "isolatedModules": true,
12 | "esModuleInterop": true,
13 | "noUnusedLocals": true,
14 | "noUnusedParameters": true,
15 | "noImplicitReturns": true,
16 | "skipLibCheck": true,
17 | "baseUrl": ".",
18 | "paths": {
19 | "@/*": ["lib/*"],
20 | // https://github.com/microsoft/TypeScript/issues/29808#issuecomment-540292885
21 | "@vueuse/shared": ["node_modules/@vueuse/shared/"]
22 | },
23 | "types": ["vite/client", "vitest/globals"],
24 | "declarationDir": "./dist/types",
25 | "declaration": true,
26 | },
27 | "include": ["lib"]
28 | }
29 |
--------------------------------------------------------------------------------
/docs/data-fetching.md:
--------------------------------------------------------------------------------
1 | # Data Fetching
2 |
3 | The basic use is of `useSWR` composable is:
4 |
5 | ```ts
6 | const { data, error } = useSWR(key, fetcher);
7 | ```
8 |
9 | Here, the `fetcher` is a function and receives the key as the first argument, then return the data to be cached.
10 |
11 | The returned value will be avaiable in `data` and if fetcher throws, the error will be caught in `error`.
12 |
13 | ## Fetcher
14 |
15 | You can use any library to fetch the data.
16 |
17 | ### Native `fetch`
18 |
19 | ```ts
20 | import { useSWR } from 'swr-vue'
21 |
22 | const fetcher = url => fetch(url).then(response => response.json())
23 |
24 | const { data, error } = useSWR('/api/todos', fetcher)
25 | ```
26 |
27 | ### Axios
28 |
29 | ```ts
30 | import { useSWR } from 'swr-vue'
31 | import axios from 'axios'
32 |
33 | const fetcher = url => axios.get(url).then(res => res.data);
34 |
35 | const { data, error } = useSWR('/api/data', fetcher);
36 | ```
37 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import { resolve } from 'node:path';
4 |
5 | import { defineConfig } from 'vite';
6 | import ViteDts from 'vite-plugin-dts';
7 |
8 | export default defineConfig({
9 | plugins: [ViteDts({ outputDir: resolve(__dirname, 'dist', 'types') })],
10 |
11 | resolve: {
12 | alias: {
13 | '@': resolve(__dirname, 'lib'),
14 | },
15 | },
16 |
17 | build: {
18 | minify: false,
19 |
20 | lib: {
21 | entry: resolve(__dirname, 'lib/index.ts'),
22 | name: 'swr-vue',
23 | fileName: (format) => `swr-vue.${format}.js`,
24 | },
25 |
26 | rollupOptions: {
27 | external: ['vue'],
28 | output: {
29 | globals: {
30 | vue: 'Vue',
31 | },
32 | },
33 | },
34 | },
35 |
36 | test: {
37 | globals: true,
38 | environment: 'jsdom',
39 | clearMocks: true,
40 | setupFiles: ['./vitest.setup.ts'],
41 |
42 | coverage: {
43 | provider: 'istanbul',
44 | },
45 | },
46 | });
47 |
--------------------------------------------------------------------------------
/lib/composables/scope-state/index.ts:
--------------------------------------------------------------------------------
1 | import { computed, ComputedRef, toRefs, unref, watch } from 'vue';
2 | import { MaybeRef, toReactive } from '@vueuse/core';
3 |
4 | import { globalState } from '@/config';
5 | import { CacheProvider, CacheState, ScopeState } from '@/types';
6 |
7 | const initScopeState = (cacheProvider: CacheProvider) => {
8 | globalState.set(cacheProvider, { revalidateCache: new Map() });
9 | };
10 |
11 | export const useScopeState = (_cacheProvider: MaybeRef>) => {
12 | const cacheProvider = computed(() => unref(_cacheProvider));
13 | const scopeState = computed(() => globalState.get(cacheProvider.value));
14 |
15 | const onScopeStateChange = () => {
16 | if (!scopeState.value) initScopeState(cacheProvider.value);
17 | };
18 |
19 | watch(scopeState, onScopeStateChange, { immediate: true });
20 |
21 | return {
22 | scopeState: scopeState as ComputedRef,
23 | ...toRefs(toReactive(scopeState as ComputedRef)),
24 | };
25 | };
26 |
--------------------------------------------------------------------------------
/docs/global-configuration.md:
--------------------------------------------------------------------------------
1 | # Global Configuration
2 |
3 | You can use `configureGlobalSWR` function to create a configuration scope (this uses [provide/inject](https://vuejs.org/guide/components/provide-inject.html) under the hood) and provide a sharable configuration to all composables under this scope.
4 |
5 | ```ts
6 | import { configureGlobalSWR } from 'swr-vue';
7 |
8 | configureGlobalSWR(options)
9 | ```
10 |
11 | > [Available options](./options.md)
12 |
13 | ## Extra options
14 |
15 | ### Cache Provider
16 |
17 | `configureGlobalSWR` also accepts an optional cache provider.
18 |
19 | ```ts
20 | configureGlobalSWR({ cacheProvider: new Map() });
21 | ```
22 |
23 | ## Access Current Scope Configurations
24 |
25 | You can use the `useSWRConfig` composable to get the scope configurations, as well as mutate and cache.
26 |
27 | ```ts
28 | import { useSWRConfig } from 'swr-vue';
29 |
30 | const { config, mutate } = useSWRConfig();
31 | ```
32 |
33 | Nested configurations will be extended. If no `configureGlobalSWR` is used, it will return the default ones.
34 |
--------------------------------------------------------------------------------
/lib/composables/swr/swr-ssr.spec.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @vitest-environment node
3 | */
4 |
5 | import { SWRComposableConfig } from '@/types';
6 | import { useSetupInServer } from '@/utils/test';
7 |
8 | import { useSWR } from '.';
9 |
10 | const defaultKey = 'defaultKey';
11 | const defaultFetcher = vi.fn((key: string) => key);
12 |
13 | describe('useSWR - Cache', () => {
14 | beforeEach(() => {
15 | vi.useRealTimers();
16 | });
17 |
18 | it('should not throw when rendered in server with default options', async () => {
19 | await useSetupInServer(() => {
20 | expect(() => useSWR(defaultKey, defaultFetcher)).not.toThrow();
21 | });
22 |
23 | expect.assertions(1);
24 | });
25 |
26 | it('should not throw when rendered in server with options using browser APIs', async () => {
27 | const config: SWRComposableConfig = {
28 | revalidateOnFocus: true,
29 | revalidateOnReconnect: true,
30 | };
31 |
32 | await useSetupInServer(() => {
33 | expect(() => useSWR(defaultKey, defaultFetcher, config)).not.toThrow();
34 | });
35 |
36 | expect.assertions(1);
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Eduardo Wesley
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths-ignore:
8 | - '**/docs/**'
9 |
10 | jobs:
11 | release:
12 | name: Release
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v3
17 | with:
18 | fetch-depth: 0
19 | token: ${{ secrets.CI_GITHUB_TOKEN }}
20 |
21 | - name: Setup Node.js
22 | uses: actions/setup-node@v2
23 | with:
24 | node-version: 16
25 |
26 | - name: Configure corepack
27 | run: corepack enable
28 |
29 | - name: Install dependencies
30 | run: pnpm i
31 |
32 | - name: Check lint
33 | run: pnpm run lint:js
34 |
35 | - name: Check Tests
36 | run: pnpm run test
37 |
38 | - name: Build lib
39 | run: pnpm run build
40 |
41 | - name: Check Types
42 | run: pnpm run test:types
43 |
44 | - name: Release
45 | env:
46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
47 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
48 | run: pnpm run generate:release
49 |
--------------------------------------------------------------------------------
/lib/composables/swr/swr-cache.spec.ts:
--------------------------------------------------------------------------------
1 | import { nextTick, ref } from 'vue';
2 |
3 | import { SWRComposableConfig } from '@/types';
4 | import { mockedCache, useInjectedSetup } from '@/utils/test';
5 | import { serializeKey } from '@/utils';
6 |
7 | import { useSWR } from '.';
8 | import { configureGlobalSWR } from '../global-swr-config';
9 |
10 | const cacheProvider = mockedCache;
11 | const defaultKey = 'defaultKey';
12 | const defaultFetcher = vi.fn((key: string) => key);
13 | const defaultOptions: SWRComposableConfig = { dedupingInterval: 0 };
14 |
15 | describe('useSWR - Cache', () => {
16 | beforeEach(() => {
17 | vi.useRealTimers();
18 | cacheProvider.clear();
19 |
20 | vi.spyOn(navigator, 'onLine', 'get').mockReturnValue(true);
21 | vi.spyOn(document, 'visibilityState', 'get').mockReturnValue('visible');
22 | });
23 |
24 | it('should set an cache instance to key if not exists', async () => {
25 | const key = ref(defaultKey);
26 | const keyTwo = 'key-two';
27 |
28 | useInjectedSetup(
29 | () => configureGlobalSWR({ cacheProvider }),
30 | () => useSWR(key, defaultFetcher, defaultOptions),
31 | );
32 |
33 | expect(cacheProvider.has(serializeKey(key.value).key)).toBeTruthy();
34 |
35 | key.value = keyTwo;
36 |
37 | await nextTick();
38 | expect(cacheProvider.has(serializeKey(key.value).key)).toBeTruthy();
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/docs/conditional-fetching.md:
--------------------------------------------------------------------------------
1 | # Conditional Fetching
2 |
3 | ## Conditional
4 |
5 | Use any [falsy](https://developer.mozilla.org/docs/Glossary/Falsy) value or pass a function as key to conditionally fetch data. If the function throws or returns a [falsy](https://developer.mozilla.org/docs/Glossary/Falsy) value, the `useSWR` will not start the request.
6 |
7 | ```ts
8 | // conditionally fetch
9 | const { data } = useSWR(shouldFetch ? '/api/todos' : null, fetcher);
10 |
11 | // ...or return a falsy value
12 | const { data } = useSWR(() => shouldFetch ? '/api/todos' : null, fetcher);
13 |
14 | // ...or throw an error when todo.id is not defined
15 | const { data } = useSWR(() => `/api/todos/${todo.id}`, fetcher);
16 | ```
17 |
18 | ## Dependent
19 |
20 | `useSWR` also allows you to fetch data that depends on other data. It allows serial fetching when a piece of dynamic data is required for the next data fetch to happen.
21 |
22 | ```vue
23 |
32 |
33 |
34 |
loading...
35 |
You have {{ projects.length }} projects
36 |
37 | ```
38 |
--------------------------------------------------------------------------------
/docs/revalidation.md:
--------------------------------------------------------------------------------
1 | # Automatic Revalidation
2 |
3 | If you want to manually revalidate the data, you can disable revaldation with [options](./options.md) and use [mutation](./mutation.md).
4 |
5 | ## Revalidate on Focus
6 |
7 | When you re-focus a page or switch between tabs, `useSWR` automatically revalidates data.
8 |
9 | This can be useful to synchronize immediately with the latest state. This is useful for refreshing data in situations like stale mobile tabs or a laptop that has gone to sleep.
10 |
11 | > This feature is enabled by default but can be disabled via the [revalidateOnFocus](./options.md) option.
12 |
13 | ## Revalidate on Interval
14 |
15 | In many cases, data changes due to multiple devices, multiple users, multiple tabs. How can we update the data on the screen over time?
16 |
17 | `useSWR` will give you the option to automatically refetch data. It’s will only happen if the component associated with the hook is mounted.
18 |
19 | > This feature is disabled by default but can be enabled via the [refreshInterval](./options.md) option.
20 |
21 | > There are also options such as `refreshWhenHidden` and `refreshWhenOffline`. Both are disabled by default so `useSWR` will not fetch when the webpage is not on screen, or there is no network connection
22 |
23 | ## Revalidate on Reconnect
24 |
25 | It is also useful revalidate when the user is back online. This scenario happens a lot when the user unlocks their computer, but the internet is not yet connected at the same time.
26 |
27 | > This feature is enabled by default but can be disabled via the [revalidateOnReconnect](./options.md) option.
28 |
--------------------------------------------------------------------------------
/lib/composables/scope-state/scope-state.spec.ts:
--------------------------------------------------------------------------------
1 | import { useInjectedSetup, mockedCache } from '@/utils/test';
2 | import { globalState } from '@/config';
3 |
4 | import { useScopeState } from '.';
5 |
6 | const cacheProvider = mockedCache;
7 |
8 | const useScopeStateWrapped = () =>
9 | useInjectedSetup(
10 | () => {},
11 | () => useScopeState(cacheProvider),
12 | );
13 |
14 | describe('useSWR - mutate', () => {
15 | beforeEach(() => {
16 | cacheProvider.clear();
17 | });
18 |
19 | it('should init scope in case that the scope does not have values', async () => {
20 | const { scopeState, revalidateCache } = useScopeStateWrapped();
21 |
22 | expect(scopeState.value).not.toBeUndefined();
23 | expect(scopeState.value.revalidateCache).toBeInstanceOf(Map);
24 |
25 | // toRefs values
26 | expect(revalidateCache.value).toBeInstanceOf(Map);
27 | });
28 |
29 | it('should return values for current scope', async () => {
30 | const key = 'key1';
31 | const revalidateCb = vi.fn();
32 |
33 | globalState.set(cacheProvider, {
34 | revalidateCache: new Map([[key, [revalidateCb]]]),
35 | });
36 |
37 | const { scopeState, revalidateCache } = useScopeStateWrapped();
38 |
39 | expect(scopeState.value.revalidateCache).toBeInstanceOf(Map);
40 | expect(scopeState.value.revalidateCache.size).toBe(1);
41 | expect(scopeState.value.revalidateCache.get(key)).toHaveLength(1);
42 | expect(scopeState.value.revalidateCache.get(key)).toContain(revalidateCb);
43 |
44 | // toRefs values
45 | expect(revalidateCache.value).toBeInstanceOf(Map);
46 | expect(revalidateCache.value.size).toBe(1);
47 | expect(revalidateCache.value.get(key)).toHaveLength(1);
48 | expect(revalidateCache.value.get(key)).toContain(revalidateCb);
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/lib/utils/serialize-key/serialize-key.spec.ts:
--------------------------------------------------------------------------------
1 | import { ref } from 'vue';
2 |
3 | import { stableHash as hash } from '@/utils';
4 |
5 | import { serializeKey } from '.';
6 |
7 | describe('serializeKey', () => {
8 | it('should return the generated key', () => {
9 | const sourceKey = 'key-home';
10 | const { key } = serializeKey(sourceKey);
11 |
12 | expect(key).toBe(hash(sourceKey));
13 | });
14 |
15 | it('should return empty key when fail the function key', () => {
16 | const sourceKey = () => {
17 | throw new Error('Failed');
18 | };
19 | const { key } = serializeKey(sourceKey);
20 |
21 | expect(key).toBe('');
22 | });
23 |
24 | it('should return the result of the function key', () => {
25 | const sourceKey = () => 'return';
26 | const { key } = serializeKey(sourceKey);
27 |
28 | expect(key).toBe('"return"');
29 | });
30 |
31 | it.each([
32 | '',
33 | false as const,
34 | null,
35 | undefined,
36 | [],
37 | () => '',
38 | () => false as const,
39 | () => null,
40 | () => undefined,
41 | () => [],
42 | ])(
43 | 'should return empty string if key resolves to a falsy value or empty array: "%s"',
44 | (sourceKey) => {
45 | const { key } = serializeKey(sourceKey);
46 |
47 | expect(key).toBe('');
48 | },
49 | );
50 |
51 | it.each([
52 | ref(''),
53 | ref(false as const),
54 | ref(null),
55 | ref(undefined),
56 | ref([]),
57 | ref(() => ''),
58 | ref(() => false as const),
59 | ref(() => null),
60 | ref(() => undefined),
61 | ref(() => []),
62 | ])(
63 | 'should return empty string if ref key resolves to a falsy value or empty array',
64 | (sourceKey) => {
65 | const { key } = serializeKey(sourceKey);
66 |
67 | expect(key).toBe('');
68 | },
69 | );
70 | });
71 |
--------------------------------------------------------------------------------
/lib/composables/swr/swr-types.test-d.ts:
--------------------------------------------------------------------------------
1 | import { expectType } from '@/utils/type-assertions';
2 |
3 | import { useSWR } from '.';
4 |
5 | type Person = {
6 | name: string;
7 | id: string;
8 | age: number;
9 | };
10 |
11 | // Infered types
12 | {
13 | const { data: data1 } = useSWR('key', () => 'string');
14 | const { data: data2 } = useSWR('key', () => 1);
15 | const { data: data3 } = useSWR('key', () => ['string']);
16 | const { data: data4 } = useSWR('key', () => false);
17 | const { data: data5 } = useSWR('key', () => ({} as Person));
18 |
19 | expectType(data1.value);
20 | expectType(data2.value);
21 | expectType(data3.value);
22 | expectType(data4.value);
23 | expectType(data5.value);
24 | }
25 |
26 | // Generics
27 | {
28 | const { data: data1, error: error1 } = useSWR('key', () => 2);
29 |
30 | expectType(data1.value);
31 | expectType(error1.value);
32 |
33 | interface CustomError extends Error {
34 | name: 'custom-error';
35 | }
36 |
37 | const { data: data2, error: error2 } = useSWR('key', () => 'return');
38 |
39 | expectType(data2.value);
40 | expectType(error2.value);
41 |
42 | interface CustomError extends Error {
43 | name: 'custom-error';
44 | }
45 |
46 | /// @ts-expect-error Generic type differ from return type
47 | useSWR('key', () => 2);
48 | /// @ts-expect-error Generic type differ from return type
49 | useSWR('key', () => '');
50 | /// @ts-expect-error Generic type differ from return type
51 | useSWR('key', () => '');
52 | // Gneric match return error - Expect no error
53 | useSWR('key', () => ({} as Person));
54 | }
55 |
--------------------------------------------------------------------------------
/lib/utils/check-types/check-types.spec.ts:
--------------------------------------------------------------------------------
1 | import { isFunction, isDate, isUndefined } from '.';
2 |
3 | beforeEach(() => {
4 | vi.useRealTimers();
5 | });
6 |
7 | describe('isFunction', () => {
8 | it('should return true if value is a function', () => {
9 | expect(isFunction(() => ({}))).toBe(true);
10 | expect(isFunction(() => [])).toBe(true);
11 | expect(isFunction(() => true)).toBe(true);
12 | expect(isFunction(() => 'foo')).toBe(true);
13 | expect(isFunction(true)).toBe(false);
14 | expect(isFunction([])).toBe(false);
15 | expect(isFunction({})).toBe(false);
16 | expect(isFunction('')).toBe(false);
17 | expect(isFunction(1)).toBe(false);
18 | expect(isFunction(new Date())).toBe(false);
19 | });
20 | });
21 |
22 | describe('isDate', () => {
23 | it('should return true if value is a date', () => {
24 | expect(isDate(new Date())).toBe(true);
25 | expect(isDate(new Date('1970-01-01T00:00:00.000Z'))).toBe(true);
26 | expect(isDate('1970-01-01T00:00:00.000Z')).toBe(false);
27 | expect(isDate(() => ({}))).toBe(false);
28 | expect(isDate(true)).toBe(false);
29 | expect(isDate([])).toBe(false);
30 | expect(isDate({})).toBe(false);
31 | expect(isDate('')).toBe(false);
32 | expect(isDate(1)).toBe(false);
33 | });
34 | });
35 |
36 | describe('isUndefined', () => {
37 | it('should return true if value is undefined', () => {
38 | // Primitives
39 | expect(isUndefined('key')).toBeFalsy();
40 | expect(isUndefined(1)).toBeFalsy();
41 | expect(isUndefined('false')).toBeFalsy();
42 | expect(isUndefined(false)).toBeFalsy();
43 | expect(isUndefined(true)).toBeFalsy();
44 | expect(isUndefined(null)).toBeFalsy();
45 | expect(isUndefined('null')).toBeFalsy();
46 | expect(isUndefined(undefined)).toBeTruthy();
47 | expect(isUndefined(NaN)).toBeFalsy();
48 | expect(isUndefined(Infinity)).toBeFalsy();
49 | expect(isUndefined('')).toBeFalsy();
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/docs/.vitepress/config.js:
--------------------------------------------------------------------------------
1 | import { version } from '../../package.json';
2 |
3 | export default {
4 | lang: 'en-US',
5 | title: 'SWR Vue',
6 | lastUpdated: true,
7 | base: '/swr-vue/',
8 |
9 | themeConfig: {
10 | nav: nav(),
11 |
12 | sidebar: sidebar(),
13 |
14 | socialLinks: [
15 | { icon: 'github', link: 'https://github.com/edumudu/swr-vue' }
16 | ],
17 |
18 | footer: {
19 | message: 'Released under the MIT License.',
20 | },
21 |
22 | editLink: {
23 | pattern: 'https://github.com/edumudu/swr-vue/edit/main/docs/:path',
24 | text: 'Edit this page on GitHub'
25 | },
26 | }
27 | }
28 |
29 | function nav() {
30 | return [
31 | {
32 | text: version,
33 | items: [
34 | {
35 | text: 'Release Notes',
36 | link: 'https://github.com/edumudu/swr-vue/releases'
37 | },
38 | {
39 | text: 'Contributing',
40 | link: 'https://github.com/edumudu/swr-vue/blob/main/CONTRIBUTING.md'
41 | },
42 | ],
43 | },
44 | ]
45 | }
46 |
47 | function sidebar() {
48 | return [
49 | {
50 | text: 'Introduction',
51 | items: [
52 | { text: 'About', link: '/introduction' },
53 | { text: 'Getting started', link: '/getting-started' },
54 | ]
55 | },
56 | {
57 | text: 'Usage',
58 | items: [
59 | { text: 'Mutation', link: '/mutation' },
60 | { text: 'Global Configuration', link: '/global-configuration' },
61 | { text: 'Data Fetching', link: '/data-fetching' },
62 | { text: 'Error Handling', link: '/error-handling' },
63 | { text: 'Revalidation', link: '/revalidation' },
64 | { text: 'Conditional Fetching', link: '/conditional-fetching' },
65 | { text: 'Typescript', link: '/typescript' },
66 | ]
67 | },
68 | {
69 | text: 'API',
70 | items: [
71 | { text: 'Options', link: '/options' },
72 | ]
73 | },
74 | ]
75 | }
76 |
--------------------------------------------------------------------------------
/docs/options.md:
--------------------------------------------------------------------------------
1 | # API Options
2 |
3 | ```js
4 | const { data, error, isValidating, mutate } = useSWR(key, fetcher, options)
5 | ```
6 |
7 | ## Parameters
8 |
9 | - `key` - A unique key for the request. Can be an `string | array | function | falsy value`
10 | - `fetcher` - A Promise that resolves to the data that you want to use
11 | - `options` - (optional) an object of options for this SWR composable
12 |
13 | ## Return Values
14 |
15 | - `data` - data for the given key resolved by fetcher (or undefined if not loaded)
16 | - `error` - error thrown by fetcher (or undefined if nothing threw)
17 | - `isValidating` - if there's the first request or revalidation going on
18 | - `mutate(updateFn, options?)` - function to mutate the cached data. [More details](./mutation.md)
19 |
20 | ## Options
21 |
22 | - `revalidateOnFocus = true` - Automatically revalidate when window gets focused
23 | - `revalidateOnReconnect = true` - Automatically revalidate when the browser regains a network connection
24 | - `revalidateIfStale = true` - Automatically revalidate if there is stale data
25 | - `dedupingInterval = 2000` - dedupe requests with the same key in this time span in milliseconds
26 | - `fallback` - a key-value object of multiple fallback data
27 | - `fallbackData` - initial data to be returned (this has priority over `fallback` option)
28 | - `focusThrottleInterval = 5000` - only revalidate on focus once during a time span in milliseconds
29 | - `refreshInterval = 0` - [(details)](./revalidation.md#revalidate-on-interval)
30 | - Disabled by default: `refreshInterval = 0`
31 | - If set to a number, polling interval in milliseconds
32 | - If set to a function, the function will receive the latest data and should return the interval in milliseconds
33 | - `refreshWhenHidden = false` - polling when the window is invisible (if refreshInterval is enabled),
34 | - `refreshWhenOffline = false` - polling when the browser is offline (determined by navigator.onLine),
35 | - `onSuccess(data, key, config)` - callback function when a request finishes successfully
36 | - `onError(err, key, config)` - callback function when a request returns an error
37 |
--------------------------------------------------------------------------------
/lib/devtools.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable eslint-comments/disable-enable-pair */
2 | /* eslint-disable no-param-reassign */
3 | import { App, PluginDescriptor, setupDevtoolsPlugin, DevtoolsPluginApi } from '@vue/devtools-api';
4 | import { watch } from 'vue';
5 |
6 | import { defaultConfig } from '@/config';
7 |
8 | type AnyFunction = (...args: any[]) => any;
9 |
10 | type HookType<
11 | TApi extends DevtoolsPluginApi,
12 | THookName extends keyof TApi['on'],
13 | T = TApi['on'][THookName],
14 | > = T extends AnyFunction ? Parameters[0] : never;
15 |
16 | const inspectorId = 'swr-vue-inspector';
17 |
18 | export function setupDevtools(app: App) {
19 | const setupPluginSettings: PluginDescriptor = {
20 | id: 'swr-vue-devtools-plugin',
21 | label: 'Stale-While-Revalidate Vue',
22 | packageName: 'swr-vue',
23 | homepage: 'https://edumudu.github.io/swr-vue/',
24 | app,
25 | };
26 |
27 | const isSwrInspector = (id: string) => id === inspectorId;
28 |
29 | setupDevtoolsPlugin(setupPluginSettings, (api) => {
30 | const handleGetInspectorTree: HookType = (payload) => {
31 | if (!isSwrInspector(inspectorId)) return;
32 |
33 | payload.rootNodes = [
34 | {
35 | id: 'root',
36 | label: 'Global scope',
37 | },
38 | ];
39 | };
40 |
41 | const handleGetInspectorState: HookType = (payload) => {
42 | if (!isSwrInspector(inspectorId) || payload.nodeId !== 'root') return;
43 |
44 | const entries = Array.from(defaultConfig.cacheProvider.entries(), ([key, value]) => ({
45 | key,
46 | value,
47 | }));
48 |
49 | payload.state = {
50 | 'Global cache': entries,
51 | };
52 | };
53 |
54 | api.addInspector({
55 | id: inspectorId,
56 | label: 'SWR',
57 | icon: 'archive',
58 | });
59 |
60 | api.on.getInspectorTree(handleGetInspectorTree);
61 | api.on.getInspectorState(handleGetInspectorState);
62 |
63 | watch(defaultConfig.cacheProvider, () => api.sendInspectorState(inspectorId), { deep: true });
64 | });
65 | }
66 |
--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
1 | # Getting Started
2 |
3 | ## Installation
4 |
5 | Inside your vue project directory, run the following:
6 |
7 | With pnpm
8 | ```bash
9 | pnpm add swr-vue
10 | ```
11 |
12 | With yarn
13 | ```bash
14 | yarn add swr-vue
15 | ```
16 |
17 | With npm
18 | ```bash
19 | npm install swr-vue
20 | ```
21 |
22 | ## Quick Start
23 |
24 | First, you need to create a `fetcher` function, for normal RESTful APIs with JSON data, you can just create a wrapper of the native `fetch` function:
25 |
26 | ```ts
27 | const fetcher = (...args) => fetch(...args).then(res => res.json());
28 | ```
29 |
30 | then you import you import `useSWR` and can start using inside components
31 |
32 |
33 |
34 | ```vue
35 |
38 |
39 |
40 |
failed to load the article
41 |
loading...
42 |
{{ data.title }}!
43 |
44 | ```
45 |
46 | Normally, there're 3 possible states of a request: "pending", "ready", or "error". You can use the variables `data` and `error` to determine the current state of the request.
47 |
48 | ## Reusability
49 |
50 | When building an app, you may need to reuse the same data in diferent places. You can create a reusable composable on top of `useSWR` as following:
51 |
52 | ```js
53 | const useArticle = (id) => {
54 | const { data, error } = useSWR(`/api/article/${id}`, fetcher);
55 |
56 | return {
57 | error,
58 | article: data,
59 | hasError: computed(() => !!error.value),
60 | isLoading: computed(() => !error.value && !data.value),
61 | }
62 | }
63 | ```
64 |
65 | and use it in the components
66 |
67 | ```vue
68 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | ```
79 |
80 | By doing this you can fetch data in a more declarative way. Forget about start the request, update loading, and return the final result.
81 | You just need to specify the data used by the component
82 |
--------------------------------------------------------------------------------
/docs/error-handling.md:
--------------------------------------------------------------------------------
1 | # Error Handling
2 |
3 | If an error is thrown inside [fetcher](./data-fetching), it will be returned as `error` by the composable
4 |
5 | ```ts
6 | // ...
7 | const { data, error } = useSWR('/api/user', fetcher);
8 | ```
9 |
10 | The `error` object will be defined if the fetch promise is rejected or `fetcher` contains an syntax error.
11 |
12 | > Note that `data` and `error` can exist at the same time. So the UI can display the existing data, while knowing the upcoming request has failed.
13 |
14 | ## Status Code and Error Object
15 |
16 | Sometimes we want the API to return an error along with the status code. Both are useful for the client.
17 |
18 | We may arrange for our `fetcher` to return additional information. If the status code is not 2xx, we treat it as an error even though it can be parsed as JSON:
19 |
20 | ```ts
21 | const fetcher = async url => {
22 | const res = await fetch(url)
23 |
24 | // If the status code is not in the range 200-299,
25 | // we still try to parse and throw it.
26 | if (!res.ok) {
27 | const error = new Error('An error occurred while fetching the data.')
28 |
29 | // Attach extra info to the error object.
30 | error.info = await res.json()
31 | error.status = res.status
32 |
33 | throw error
34 | }
35 |
36 | return res.json()
37 | }
38 |
39 | // ...
40 | const { error } = useSWR('/api/user', fetcher)
41 | // error.value.info === {
42 | // message: "You are not authorized to access this resource.",
43 | // documentation_url: "..."
44 | // }
45 | // error.value.status === 403
46 | ```
47 |
48 | ## Global Error Report
49 |
50 | You can always get the `error` object inside the component reactively. But in case you want to handle the error globally, to notify the UI to show a toast or a snackbar, or report it somewhere such as [Sentry](https://sentry.io/), there's an `onError` event:
51 |
52 | ```ts
53 | configureGlobalSWR({
54 | onError: (error, key) => {
55 | // We can send the error to Sentry,
56 |
57 | if (error.status !== 403 && error.status !== 404) {
58 | // or show a notification UI.
59 | }
60 | }
61 | })
62 | ```
63 |
64 | This is also available in the composable options.
65 |
66 | In case that you pass an global `onError` and in a composable inside the same context also pass a `onError` the two of them will be called. First the local one followed by the global
67 |
--------------------------------------------------------------------------------
/lib/utils/merge-config/merge-config.spec.ts:
--------------------------------------------------------------------------------
1 | import { mergeConfig } from '.';
2 |
3 | describe('mergeConfig', () => {
4 | it.each([
5 | [{}, { revalidateIfStale: true }, { revalidateIfStale: true }],
6 | [{ revalidateIfStale: true }, {}, { revalidateIfStale: true }],
7 | [{ revalidateIfStale: true }, { revalidateIfStale: true }, { revalidateIfStale: true }],
8 | [{ revalidateIfStale: false }, { revalidateIfStale: true }, { revalidateIfStale: true }],
9 | [{ revalidateIfStale: true }, { revalidateIfStale: false }, { revalidateIfStale: false }],
10 | [
11 | { revalidateIfStale: false, revalidateOnFocus: true },
12 | { revalidateIfStale: false },
13 | { revalidateIfStale: false, revalidateOnFocus: true },
14 | ],
15 | [
16 | { revalidateIfStale: true },
17 | { revalidateIfStale: true, revalidateOnFocus: true },
18 | { revalidateIfStale: true, revalidateOnFocus: true },
19 | ],
20 | ])('should merge two configs: %s', (obj1, obj2, expectedResult) => {
21 | expect(mergeConfig(obj1, obj2)).toEqual(expectedResult);
22 | });
23 |
24 | it('should also merge callbacks', () => {
25 | const globalConfig = Object.freeze({
26 | onError: vi.fn(),
27 | onSuccess: vi.fn(),
28 | });
29 |
30 | const localConfig = Object.freeze({
31 | onError: vi.fn(),
32 | onSuccess: vi.fn(),
33 | });
34 |
35 | const { onError, onSuccess } = mergeConfig(globalConfig, localConfig);
36 |
37 | onError();
38 | onSuccess();
39 |
40 | expect(globalConfig.onSuccess).toHaveBeenCalledOnce();
41 | expect(globalConfig.onError).toHaveBeenCalledOnce();
42 | expect(localConfig.onSuccess).toHaveBeenCalledOnce();
43 | expect(localConfig.onError).toHaveBeenCalledOnce();
44 | });
45 |
46 | it('should not throw when one of the callbacks is missing', () => {
47 | const globalConfig = Object.freeze({ onSuccess: vi.fn() });
48 | const localConfig = Object.freeze({ onError: vi.fn() });
49 |
50 | const { onError, onSuccess } = mergeConfig(globalConfig, localConfig);
51 |
52 | onError();
53 | onSuccess();
54 |
55 | expect(globalConfig.onSuccess).toHaveBeenCalledOnce();
56 | expect(localConfig.onError).toHaveBeenCalledOnce();
57 | });
58 |
59 | it('should return undefined when callback is missing in either configs', () => {
60 | const { onError, onSuccess } = mergeConfig({}, {});
61 |
62 | expect(onError).toBeUndefined();
63 | expect(onSuccess).toBeUndefined();
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
13 |
14 | ## Introduction
15 |
16 | `swr-vue` is a Vue composables library for data fetching.
17 |
18 | The name “**SWR**” is derived from `stale-while-revalidate`, a cache invalidation strategy popularized by [HTTP RFC 5861](https://tools.ietf.org/html/rfc5861).
19 | **SWR** first returns the data from cache (stale), then sends the request (revalidate), and finally comes with the up-to-date data again.
20 |
21 | #### Features
22 |
23 | - Fast and reusable data fetching
24 | - Transport and protocol agnostic
25 | - Built-in cache and request dedulication
26 | - Revalidation on focus
27 | - Revalidation on network recovery
28 | - Polling
29 | - Local mutation (Optimistic UI)
30 | - Type safe
31 | - Configurable
32 |
33 | For a more details you can visit the [documentation](https://edumudu.github.io/swr-vue/)
34 |
35 | If you are looking to contribute, please check [CONTRIBUTING.md](./CONTRIBUTING.md)
36 |
37 | ## Quick Start
38 |
39 | ```vue
40 |
51 |
52 |
53 |
failed to load
54 |
loading...
55 |
hello {{ data.name }}!
56 |
57 | ```
58 |
59 | In this example, the composable `useSWR` accepts a `key` and a `fetcher` function.
60 | The `key` is a unique identifier of the request, normally the URL of the API. And the `fetcher` accepts
61 | `key` as its parameter and returns the data asynchronously.
62 |
63 | `useSWR` returns 2 values: `data` and `error`. When the request is not yet finished,
64 | `data` and `error` will be `undefined`. And when we get a response, it sets `data` and `error` based on the result
65 | of `fetcher`.
66 |
67 | `fetcher` can be any asynchronous function, you can use your favourite data-fetching library to handle that part.
68 |
69 |
70 |
71 | ## Thanks
72 | `swr-vue` is inspired by these great works:
73 |
74 | - [vueuse](https://github.com/antfu/vueuse)
75 | - [vercel/swr](https://github.com/vercel/swr)
76 |
77 | ## License
78 |
79 | The [MIT](LICENSE) License.
80 |
--------------------------------------------------------------------------------
/lib/utils/stable-hash/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Thanks to vercel/swr
3 | * https://github.com/vercel/swr/blob/main/_internal/utils/hash.ts
4 | */
5 | import { createUnrefFn } from '@vueuse/core';
6 |
7 | import { isUndefined } from '@/utils/check-types';
8 |
9 | // use WeakMap to store the object->key mapping
10 | // so the objects can be garbage collected.
11 | // WeakMap uses a hashtable under the hood, so the lookup
12 | // complexity is almost O(1).
13 | const table = new WeakMap