From 3a5aecf992e9d50ec27b4a94146d2a4b3ba35be1 Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Thu, 3 Nov 2022 23:13:32 +0100 Subject: [PATCH 01/15] feat: vue-query client persister --- packages/vue-query/src/vueQueryPlugin.ts | 25 +++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/vue-query/src/vueQueryPlugin.ts b/packages/vue-query/src/vueQueryPlugin.ts index 2333b04f02..28c9e35bb8 100644 --- a/packages/vue-query/src/vueQueryPlugin.ts +++ b/packages/vue-query/src/vueQueryPlugin.ts @@ -12,21 +12,20 @@ declare global { } } -export interface AdditionalClient { - queryClient: QueryClient - queryClientKey: string -} +type ClientPersister = (client: QueryClient) => [() => void, Promise] -interface ConfigOptions { - queryClientConfig?: MaybeRefDeep +interface CommonOptions { queryClientKey?: string contextSharing?: boolean + clientPersister?: ClientPersister +} + +interface ConfigOptions extends CommonOptions { + queryClientConfig?: MaybeRefDeep } -interface ClientOptions { +interface ClientOptions extends CommonOptions { queryClient?: QueryClient - queryClientKey?: string - contextSharing?: boolean } export type VueQueryPluginOptions = ConfigOptions | ClientOptions @@ -58,6 +57,13 @@ export const VueQueryPlugin = { } client.mount() + let persisterUnmount = () => { + // noop + }; + + if (options.clientPersister) { + [persisterUnmount] = options.clientPersister(client); + } if (process.env.NODE_ENV !== 'production' && options.contextSharing) { client @@ -69,6 +75,7 @@ export const VueQueryPlugin = { const cleanup = () => { client.unmount() + persisterUnmount(); } if (app.onUnmount) { From 0862898dd317e9bee23515f90f8c749066e156d1 Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Thu, 3 Nov 2022 23:24:46 +0100 Subject: [PATCH 02/15] fix: change devtools link to tanstack query --- packages/vue-query/src/devtools/devtools.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vue-query/src/devtools/devtools.ts b/packages/vue-query/src/devtools/devtools.ts index fce4bfcdf0..4b59a58279 100644 --- a/packages/vue-query/src/devtools/devtools.ts +++ b/packages/vue-query/src/devtools/devtools.ts @@ -21,7 +21,7 @@ export function setupDevtools(app: any, queryClient: QueryClient) { id: pluginId, label: pluginName, packageName: 'vue-query', - homepage: 'https://github.com/DamianOsipiuk/vue-query', + homepage: 'https://tanstack.com/query/v4', logo: 'https://vue-query.vercel.app/vue-query.svg', app, settings: { From afb00fb865c42de75e19867882dec40da39972eb Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Mon, 7 Nov 2022 00:06:41 +0100 Subject: [PATCH 03/15] chore: update repository in package.json --- packages/vue-query/package.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/vue-query/package.json b/packages/vue-query/package.json index 81e67c352b..b8ad9010b9 100644 --- a/packages/vue-query/package.json +++ b/packages/vue-query/package.json @@ -4,7 +4,11 @@ "description": "Hooks for managing, caching and syncing asynchronous and remote data in Vue", "author": "Damian Osipiuk", "license": "MIT", - "repository": "tanstack/query", + "repository": { + "type": "git", + "url": "https://github.com/TanStack/query.git", + "directory": "packages/vue-query" + }, "homepage": "https://tanstack.com/query", "funding": { "type": "github", From fb6232c85a8755fe3ff83f63b1d02ae3380de0c7 Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Mon, 7 Nov 2022 00:10:47 +0100 Subject: [PATCH 04/15] docs: persister example --- examples/vue/persister/.gitignore | 6 +++ examples/vue/persister/README.md | 6 +++ examples/vue/persister/index.html | 12 +++++ examples/vue/persister/package.json | 21 +++++++++ examples/vue/persister/src/App.vue | 43 ++++++++++++++++++ examples/vue/persister/src/Post.vue | 51 +++++++++++++++++++++ examples/vue/persister/src/Posts.vue | 55 +++++++++++++++++++++++ examples/vue/persister/src/main.ts | 20 +++++++++ examples/vue/persister/src/shims-vue.d.ts | 5 +++ examples/vue/persister/src/types.d.ts | 6 +++ examples/vue/persister/tsconfig.json | 15 +++++++ examples/vue/persister/vite.config.ts | 10 +++++ 12 files changed, 250 insertions(+) create mode 100644 examples/vue/persister/.gitignore create mode 100644 examples/vue/persister/README.md create mode 100644 examples/vue/persister/index.html create mode 100644 examples/vue/persister/package.json create mode 100644 examples/vue/persister/src/App.vue create mode 100644 examples/vue/persister/src/Post.vue create mode 100644 examples/vue/persister/src/Posts.vue create mode 100644 examples/vue/persister/src/main.ts create mode 100644 examples/vue/persister/src/shims-vue.d.ts create mode 100644 examples/vue/persister/src/types.d.ts create mode 100644 examples/vue/persister/tsconfig.json create mode 100644 examples/vue/persister/vite.config.ts diff --git a/examples/vue/persister/.gitignore b/examples/vue/persister/.gitignore new file mode 100644 index 0000000000..d424f6a89a --- /dev/null +++ b/examples/vue/persister/.gitignore @@ -0,0 +1,6 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local +package-lock.json diff --git a/examples/vue/persister/README.md b/examples/vue/persister/README.md new file mode 100644 index 0000000000..7573cb91da --- /dev/null +++ b/examples/vue/persister/README.md @@ -0,0 +1,6 @@ +# Basic example + +To run this example: + +- `npm install` or `yarn` +- `npm run dev` or `yarn dev` diff --git a/examples/vue/persister/index.html b/examples/vue/persister/index.html new file mode 100644 index 0000000000..547b3c19e6 --- /dev/null +++ b/examples/vue/persister/index.html @@ -0,0 +1,12 @@ + + + + + + Vue Query Example + + +
+ + + diff --git a/examples/vue/persister/package.json b/examples/vue/persister/package.json new file mode 100644 index 0000000000..cd79cb921b --- /dev/null +++ b/examples/vue/persister/package.json @@ -0,0 +1,21 @@ +{ + "name": "@tanstack/query-example-vue-basic", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "build:dev": "vite build -m development", + "serve": "vite preview" + }, + "dependencies": { + "vue": "3.2.41", + "@tanstack/vue-query": "^4.14.1", + "@tanstack/query-persist-client-core": "^4.14.1", + "@tanstack/query-sync-storage-persister": "^4.14.1" + }, + "devDependencies": { + "@vitejs/plugin-vue": "3.2.0", + "typescript": "4.8.4", + "vite": "3.2.2" + } +} diff --git a/examples/vue/persister/src/App.vue b/examples/vue/persister/src/App.vue new file mode 100644 index 0000000000..52028d9dcd --- /dev/null +++ b/examples/vue/persister/src/App.vue @@ -0,0 +1,43 @@ + + + diff --git a/examples/vue/persister/src/Post.vue b/examples/vue/persister/src/Post.vue new file mode 100644 index 0000000000..bb9f36e089 --- /dev/null +++ b/examples/vue/persister/src/Post.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/examples/vue/persister/src/Posts.vue b/examples/vue/persister/src/Posts.vue new file mode 100644 index 0000000000..12ee8ef15e --- /dev/null +++ b/examples/vue/persister/src/Posts.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/examples/vue/persister/src/main.ts b/examples/vue/persister/src/main.ts new file mode 100644 index 0000000000..5454f71a1e --- /dev/null +++ b/examples/vue/persister/src/main.ts @@ -0,0 +1,20 @@ +import { createApp } from 'vue' +import { VueQueryPlugin, type VueQueryPluginOptions } from '@tanstack/vue-query' +import { persistQueryClient } from '@tanstack/query-persist-client-core' +import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister' + +import App from './App.vue' + +const vueQueryOptions: VueQueryPluginOptions = { + queryClientConfig: { + defaultOptions: { queries: { cacheTime: 1000 * 60 * 60 * 24, staleTime: 1000 * 60 * 60 * 24 } }, + }, + clientPersister: (queryClient) => { + return persistQueryClient({ + queryClient, + persister: createSyncStoragePersister({ storage: localStorage }), + }) + }, +} + +createApp(App).use(VueQueryPlugin, vueQueryOptions).mount('#app') diff --git a/examples/vue/persister/src/shims-vue.d.ts b/examples/vue/persister/src/shims-vue.d.ts new file mode 100644 index 0000000000..daba9b9ec0 --- /dev/null +++ b/examples/vue/persister/src/shims-vue.d.ts @@ -0,0 +1,5 @@ +declare module "*.vue" { + import { DefineComponent } from "vue"; + const component: DefineComponent<{}, {}, any>; + export default component; +} diff --git a/examples/vue/persister/src/types.d.ts b/examples/vue/persister/src/types.d.ts new file mode 100644 index 0000000000..0ff5fbb96d --- /dev/null +++ b/examples/vue/persister/src/types.d.ts @@ -0,0 +1,6 @@ +export interface Post { + userId: number; + id: number; + title: string; + body: string; +} diff --git a/examples/vue/persister/tsconfig.json b/examples/vue/persister/tsconfig.json new file mode 100644 index 0000000000..e754e65292 --- /dev/null +++ b/examples/vue/persister/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "moduleResolution": "node", + "strict": true, + "jsx": "preserve", + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "lib": ["esnext", "dom"], + "types": ["vite/client"] + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/examples/vue/persister/vite.config.ts b/examples/vue/persister/vite.config.ts new file mode 100644 index 0000000000..bd15f23a1a --- /dev/null +++ b/examples/vue/persister/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()], + optimizeDeps: { + exclude: ["vue-query", "vue-demi"], + }, +}); From cef143f11559fc2e29ccb7f7de38b5b2364f796c Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Mon, 7 Nov 2022 00:13:01 +0100 Subject: [PATCH 05/15] feat: propagate isRestoring trough queryClient --- packages/vue-query/src/queryClient.ts | 3 + packages/vue-query/src/useBaseQuery.ts | 134 ++++++++++++++++------- packages/vue-query/src/vueQueryPlugin.ts | 11 +- 3 files changed, 103 insertions(+), 45 deletions(-) diff --git a/packages/vue-query/src/queryClient.ts b/packages/vue-query/src/queryClient.ts index 27d1d073f5..91f7b9c3ee 100644 --- a/packages/vue-query/src/queryClient.ts +++ b/packages/vue-query/src/queryClient.ts @@ -1,3 +1,4 @@ +import { ref } from 'vue-demi' import { QueryClient as QC } from '@tanstack/query-core' import type { QueryKey, @@ -40,6 +41,8 @@ export class QueryClient extends QC { super(vueQueryConfig) } + isRestoring = ref(false) + isFetching(filters?: MaybeRefDeep): number isFetching( queryKey?: MaybeRefDeep, diff --git a/packages/vue-query/src/useBaseQuery.ts b/packages/vue-query/src/useBaseQuery.ts index 951bbb0c3c..bdba9bb5f5 100644 --- a/packages/vue-query/src/useBaseQuery.ts +++ b/packages/vue-query/src/useBaseQuery.ts @@ -1,4 +1,13 @@ -import { onScopeDispose, toRefs, readonly, reactive, watch } from 'vue-demi' +import { + onScopeDispose, + toRefs, + readonly, + reactive, + watch, + ref, + computed, + isRef, +} from 'vue-demi' import type { ToRefs, UnwrapRef } from 'vue-demi' import type { QueryObserver, @@ -9,7 +18,7 @@ import type { } from '@tanstack/query-core' import { useQueryClient } from './useQueryClient' import { updateState, isQueryKey, cloneDeepUnref } from './utils' -import type { WithQueryClientKey } from './types' +import type { MaybeRef, WithQueryClientKey } from './types' import type { UseQueryOptions } from './useQuery' import type { UseInfiniteQueryOptions } from './useInfiniteQuery' @@ -34,7 +43,6 @@ export function useBaseQuery< TQueryFnData, TError, TData, - TQueryData, TQueryKey extends QueryKey, >( Observer: typeof QueryObserver, @@ -46,40 +54,65 @@ export function useBaseQuery< | UseQueryOptionsGeneric = {}, arg3: UseQueryOptionsGeneric = {}, ): UseQueryReturnType { - const options = getQueryUnreffedOptions() + const options = computed(() => parseQueryArgs(arg1, arg2, arg3)) + const queryClient = - options.queryClient ?? useQueryClient(options.queryClientKey) - const defaultedOptions = queryClient.defaultQueryOptions(options) - const observer = new Observer(queryClient, defaultedOptions) + options.value.queryClient ?? useQueryClient(options.value.queryClientKey) + + const defaultedOptions = computed(() => { + const defaulted = queryClient.defaultQueryOptions(options.value) + defaulted._optimisticResults = queryClient.isRestoring.value + ? 'isRestoring' + : 'optimistic' + + console.log('defaulted options recalculate', options.value, queryClient.isRestoring.value, defaulted) + return defaulted + }) + + const observer = new Observer(queryClient, defaultedOptions.value) const state = reactive(observer.getCurrentResult()) - const unsubscribe = observer.subscribe((result) => { - updateState(state, result) + + const unsubscribe = ref(() => { + // noop }) watch( - [() => arg1, () => arg2, () => arg3], + queryClient.isRestoring, + (isRestoring) => { + // isRestoring.value = val + + if (!isRestoring) { + unsubscribe.value(); + unsubscribe.value = observer.subscribe((result) => { + updateState(state, result) + }) + } + }, + { immediate: true }, + ) + + watch( + defaultedOptions, () => { - observer.setOptions( - queryClient.defaultQueryOptions(getQueryUnreffedOptions()), - ) + observer.setOptions(defaultedOptions.value) + updateState(state, observer.getCurrentResult()) }, { deep: true }, ) onScopeDispose(() => { - unsubscribe() + unsubscribe.value() }) const suspense = () => { return new Promise>((resolve) => { const run = () => { - const newOptions = queryClient.defaultQueryOptions( - getQueryUnreffedOptions(), - ) - if (newOptions.enabled !== false) { - const optimisticResult = observer.getOptimisticResult(newOptions) + if (defaultedOptions.value.enabled !== false) { + const optimisticResult = observer.getOptimisticResult( + defaultedOptions.value, + ) if (optimisticResult.isStale) { - resolve(observer.fetchOptimistic(defaultedOptions)) + resolve(observer.fetchOptimistic(defaultedOptions.value)) } else { resolve(optimisticResult) } @@ -88,7 +121,7 @@ export function useBaseQuery< run() - watch([() => arg1, () => arg2, () => arg3], run, { deep: true }) + watch(defaultedOptions, run, { deep: true }) }) } @@ -96,27 +129,44 @@ export function useBaseQuery< ...(toRefs(readonly(state)) as UseQueryReturnType), suspense, } +} - /** - * Get Query Options object - * All inner refs unwrapped. No Reactivity - */ - function getQueryUnreffedOptions() { - let mergedOptions - - if (!isQueryKey(arg1)) { - // `useQuery(optionsObj)` - mergedOptions = arg1 - } else if (typeof arg2 === 'function') { - // `useQuery(queryKey, queryFn, optionsObj?)` - mergedOptions = { ...arg3, queryKey: arg1, queryFn: arg2 } - } else { - // `useQuery(queryKey, optionsObj?)` - mergedOptions = { ...arg2, queryKey: arg1 } - } - - return cloneDeepUnref(mergedOptions) as WithQueryClientKey< - QueryObserverOptions - > +export function parseQueryArgs< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + arg1: + | MaybeRef + | MaybeRef>, + arg2: + | MaybeRef>> + | MaybeRef< + UseQueryOptionsGeneric + > = {}, + arg3: MaybeRef< + UseQueryOptionsGeneric + > = {}, +): WithQueryClientKey< + QueryObserverOptions +> { + const plainArg1 = isRef(arg1) ? arg1.value : arg1 + const plainArg2 = isRef(arg2) ? arg2.value : arg2 + const plainArg3 = isRef(arg3) ? arg3.value : arg3 + + let options = plainArg1 + + if (!isQueryKey(plainArg1)) { + options = plainArg1 + } else if (typeof plainArg2 === 'function') { + options = { ...plainArg3, queryKey: plainArg1, queryFn: plainArg2 } + } else { + options = { ...plainArg2, queryKey: plainArg1 } } + + return cloneDeepUnref(options) as WithQueryClientKey< + QueryObserverOptions + > } diff --git a/packages/vue-query/src/vueQueryPlugin.ts b/packages/vue-query/src/vueQueryPlugin.ts index 28c9e35bb8..5d5478791f 100644 --- a/packages/vue-query/src/vueQueryPlugin.ts +++ b/packages/vue-query/src/vueQueryPlugin.ts @@ -59,10 +59,15 @@ export const VueQueryPlugin = { client.mount() let persisterUnmount = () => { // noop - }; + } if (options.clientPersister) { - [persisterUnmount] = options.clientPersister(client); + client.isRestoring.value = true + const [unmount, promise] = options.clientPersister(client) + persisterUnmount = unmount + promise.then(() => { + client.isRestoring.value = false + }) } if (process.env.NODE_ENV !== 'production' && options.contextSharing) { @@ -75,7 +80,7 @@ export const VueQueryPlugin = { const cleanup = () => { client.unmount() - persisterUnmount(); + persisterUnmount() } if (app.onUnmount) { From 8d280a08c75c3d22a8eb98b3e2d7644eb91197bc Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Mon, 7 Nov 2022 00:31:41 +0100 Subject: [PATCH 06/15] fix: useIsFetching allow refs as args --- .../src/__tests__/useIsFetching.test.ts | 12 ++++++- packages/vue-query/src/useIsFetching.ts | 31 ++++++++++--------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/packages/vue-query/src/__tests__/useIsFetching.test.ts b/packages/vue-query/src/__tests__/useIsFetching.test.ts index 8475c0ab92..4eadec8244 100644 --- a/packages/vue-query/src/__tests__/useIsFetching.test.ts +++ b/packages/vue-query/src/__tests__/useIsFetching.test.ts @@ -1,4 +1,4 @@ -import { onScopeDispose, reactive } from 'vue-demi' +import { onScopeDispose, reactive, ref } from 'vue-demi' import { flushPromises, simpleFetcher } from './test-utils' import { useQuery } from '../useQuery' @@ -87,5 +87,15 @@ describe('useIsFetching', () => { expect(result).toEqual(expected) }) + + test('should unwrap refs arguments', () => { + const key = ref(['key']); + const filters = ref({ stale: ref(true) }) + + const result = parseFilterArgs(key, filters) + const expected = { queryKey: ['key'], stale: true } + + expect(result).toEqual(expected) + }) }) }) diff --git a/packages/vue-query/src/useIsFetching.ts b/packages/vue-query/src/useIsFetching.ts index f854eb608e..390b19c441 100644 --- a/packages/vue-query/src/useIsFetching.ts +++ b/packages/vue-query/src/useIsFetching.ts @@ -1,23 +1,23 @@ -import { onScopeDispose, ref, watch } from 'vue-demi' +import { computed, isRef, onScopeDispose, ref, watch } from 'vue-demi' import type { Ref } from 'vue-demi' import type { QueryKey, QueryFilters as QF } from '@tanstack/query-core' import { useQueryClient } from './useQueryClient' import { cloneDeepUnref, isQueryKey } from './utils' -import type { MaybeRefDeep, WithQueryClientKey } from './types' +import type { MaybeRef, MaybeRefDeep, WithQueryClientKey } from './types' export type QueryFilters = MaybeRefDeep> export function useIsFetching(filters?: QueryFilters): Ref export function useIsFetching( - queryKey?: QueryKey, - filters?: QueryFilters, + queryKey?: MaybeRef, + filters?: Omit, ): Ref export function useIsFetching( - arg1?: QueryKey | QueryFilters, - arg2?: QueryFilters, + arg1?: MaybeRef | QueryFilters, + arg2?: Omit, ): Ref { - const filters = ref(parseFilterArgs(arg1, arg2)) + const filters = computed(() => parseFilterArgs(arg1, arg2)) const queryClient = filters.value.queryClient ?? useQueryClient(filters.value.queryClientKey) @@ -28,9 +28,8 @@ export function useIsFetching( }) watch( - [() => arg1, () => arg2], + filters, () => { - filters.value = parseFilterArgs(arg1, arg2) isFetching.value = queryClient.isFetching(filters) }, { deep: true }, @@ -44,16 +43,18 @@ export function useIsFetching( } export function parseFilterArgs( - arg1?: QueryKey | QueryFilters, + arg1?: MaybeRef | QueryFilters, arg2: QueryFilters = {}, ) { - let options: QueryFilters + const plainArg1 = isRef(arg1) ? arg1.value : arg1 + const plainArg2 = isRef(arg2) ? arg2.value : arg2 - if (isQueryKey(arg1)) { - options = { ...arg2, queryKey: arg1 } + let options = plainArg1 + + if (isQueryKey(plainArg1)) { + options = { ...plainArg2, queryKey: plainArg1 } } else { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - options = arg1 || {} + options = plainArg1 || {} } return cloneDeepUnref(options) as WithQueryClientKey From e679120f1a8ca93f4ca0ec97e92ea9e59ffa50e4 Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Mon, 7 Nov 2022 00:38:39 +0100 Subject: [PATCH 07/15] fix: allow ref args for useIsMutating --- .../src/__tests__/useIsMutating.test.ts | 18 ++++++++--- packages/vue-query/src/useIsMutating.ts | 31 ++++++++++--------- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/packages/vue-query/src/__tests__/useIsMutating.test.ts b/packages/vue-query/src/__tests__/useIsMutating.test.ts index 37091bb198..d2d4b05b71 100644 --- a/packages/vue-query/src/__tests__/useIsMutating.test.ts +++ b/packages/vue-query/src/__tests__/useIsMutating.test.ts @@ -1,8 +1,8 @@ -import { onScopeDispose, reactive } from 'vue-demi' +import { onScopeDispose, reactive, ref } from 'vue-demi' import { flushPromises, successMutator } from './test-utils' import { useMutation } from '../useMutation' -import { parseMutationFilterArgs, useIsMutating } from '../useIsMutating' +import { parseFilterArgs, useIsMutating } from '../useIsMutating' import { useQueryClient } from '../useQueryClient' jest.mock('../useQueryClient') @@ -80,7 +80,7 @@ describe('useIsMutating', () => { describe('parseMutationFilterArgs', () => { test('should default to empty filters', () => { - const result = parseMutationFilterArgs(undefined) + const result = parseFilterArgs(undefined) expect(result).toEqual({}) }) @@ -88,10 +88,20 @@ describe('useIsMutating', () => { test('should merge mutation key with filters', () => { const filters = { fetching: true } - const result = parseMutationFilterArgs(['key'], filters) + const result = parseFilterArgs(['key'], filters) const expected = { ...filters, mutationKey: ['key'] } expect(result).toEqual(expected) }) + + test('should unwrap refs arguments', () => { + const key = ref(['key']); + const filters = ref({ fetching: ref(true) }) + + const result = parseFilterArgs(key, filters) + const expected = { queryKey: ['key'], fetching: true } + + expect(result).toEqual(expected) + }) }) }) diff --git a/packages/vue-query/src/useIsMutating.ts b/packages/vue-query/src/useIsMutating.ts index 6bcf32b6ad..ed49957172 100644 --- a/packages/vue-query/src/useIsMutating.ts +++ b/packages/vue-query/src/useIsMutating.ts @@ -1,23 +1,23 @@ -import { onScopeDispose, ref, watch } from 'vue-demi' +import { computed, isRef, onScopeDispose, ref, watch } from 'vue-demi' import type { Ref } from 'vue-demi' import type { MutationKey, MutationFilters as MF } from '@tanstack/query-core' import { useQueryClient } from './useQueryClient' import { cloneDeepUnref, isQueryKey } from './utils' -import type { MaybeRefDeep, WithQueryClientKey } from './types' +import type { MaybeRef, MaybeRefDeep, WithQueryClientKey } from './types' export type MutationFilters = MaybeRefDeep> export function useIsMutating(filters?: MutationFilters): Ref export function useIsMutating( - mutationKey?: MutationKey, + mutationKey?: MaybeRef, filters?: Omit, ): Ref export function useIsMutating( - arg1?: MutationKey | MutationFilters, + arg1?: MaybeRef | MutationFilters, arg2?: Omit, ): Ref { - const filters = ref(parseMutationFilterArgs(arg1, arg2)) + const filters = computed(() => parseFilterArgs(arg1, arg2)) const queryClient = filters.value.queryClient ?? useQueryClient(filters.value.queryClientKey) @@ -28,9 +28,8 @@ export function useIsMutating( }) watch( - [() => arg1, () => arg2], + filters, () => { - filters.value = parseMutationFilterArgs(arg1, arg2) isMutating.value = queryClient.isMutating(filters) }, { deep: true }, @@ -43,18 +42,20 @@ export function useIsMutating( return isMutating } -export function parseMutationFilterArgs( - arg1?: MutationKey | MutationFilters, +export function parseFilterArgs( + arg1?: MaybeRef | MutationFilters, arg2: MutationFilters = {}, ) { - let options: MutationFilters + const plainArg1 = isRef(arg1) ? arg1.value : arg1 + const plainArg2 = isRef(arg2) ? arg2.value : arg2 - if (isQueryKey(arg1)) { - options = { ...arg2, mutationKey: arg1 } + let options = plainArg1 + + if (isQueryKey(plainArg1)) { + options = { ...plainArg2, mutationKey: plainArg1 } } else { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - options = arg1 || {} + options = plainArg1 || {} } return cloneDeepUnref(options) as WithQueryClientKey -} +} \ No newline at end of file From ed7e008430ca40f5a530f5508700fc8de6161f03 Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Mon, 7 Nov 2022 00:49:34 +0100 Subject: [PATCH 08/15] test: add test for isRestoring --- .../src/__tests__/useIsMutating.test.ts | 2 +- .../src/__tests__/vueQueryPlugin.test.ts | 30 ++++++++++++++++++- packages/vue-query/src/useBaseQuery.ts | 1 - 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/packages/vue-query/src/__tests__/useIsMutating.test.ts b/packages/vue-query/src/__tests__/useIsMutating.test.ts index d2d4b05b71..111642f78a 100644 --- a/packages/vue-query/src/__tests__/useIsMutating.test.ts +++ b/packages/vue-query/src/__tests__/useIsMutating.test.ts @@ -99,7 +99,7 @@ describe('useIsMutating', () => { const filters = ref({ fetching: ref(true) }) const result = parseFilterArgs(key, filters) - const expected = { queryKey: ['key'], fetching: true } + const expected = { mutationKey: ['key'], fetching: true } expect(result).toEqual(expected) }) diff --git a/packages/vue-query/src/__tests__/vueQueryPlugin.test.ts b/packages/vue-query/src/__tests__/vueQueryPlugin.test.ts index c316e89546..d8cc069f6c 100644 --- a/packages/vue-query/src/__tests__/vueQueryPlugin.test.ts +++ b/packages/vue-query/src/__tests__/vueQueryPlugin.test.ts @@ -1,10 +1,11 @@ import type { App, ComponentOptions } from 'vue' -import { isVue2, isVue3 } from 'vue-demi' +import { isVue2, isVue3, ref } from 'vue-demi' import type { QueryClient } from '../queryClient' import { VueQueryPlugin } from '../vueQueryPlugin' import { VUE_QUERY_CLIENT } from '../utils' import { setupDevtools } from '../devtools/devtools' +import { flushPromises } from './test-utils' jest.mock('../devtools/devtools') @@ -258,4 +259,31 @@ describe('VueQueryPlugin', () => { expect(customClient.mount).toHaveBeenCalledTimes(1) }) }) + + describe('when persister is provided', () => { + test('should properly modify isRestoring flag on queryClient', async () => { + const appMock = getAppMock() + const customClient = { + mount: jest.fn(), + isRestoring: ref(false), + } as unknown as QueryClient + + VueQueryPlugin.install(appMock, { + queryClient: customClient, + clientPersister: () => [ + jest.fn(), + new Promise((resolve) => { + resolve() + }), + ], + }) + + expect(customClient.isRestoring.value).toBeTruthy() + + await flushPromises(); + + expect(customClient.isRestoring.value).toBeFalsy() + + }) + }) }) diff --git a/packages/vue-query/src/useBaseQuery.ts b/packages/vue-query/src/useBaseQuery.ts index bdba9bb5f5..5183133a78 100644 --- a/packages/vue-query/src/useBaseQuery.ts +++ b/packages/vue-query/src/useBaseQuery.ts @@ -65,7 +65,6 @@ export function useBaseQuery< ? 'isRestoring' : 'optimistic' - console.log('defaulted options recalculate', options.value, queryClient.isRestoring.value, defaulted) return defaulted }) From 2e9ea937356544de1be3301eefc8512a86be9db6 Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Mon, 7 Nov 2022 01:15:36 +0100 Subject: [PATCH 09/15] chore: fix formatting --- packages/vue-query/src/__tests__/useIsFetching.test.ts | 2 +- packages/vue-query/src/__tests__/useIsMutating.test.ts | 2 +- .../vue-query/src/__tests__/vueQueryPlugin.test.ts | 3 +-- packages/vue-query/src/useBaseQuery.ts | 10 ++++------ packages/vue-query/src/useIsFetching.ts | 2 +- packages/vue-query/src/useIsMutating.ts | 2 +- 6 files changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/vue-query/src/__tests__/useIsFetching.test.ts b/packages/vue-query/src/__tests__/useIsFetching.test.ts index 4eadec8244..d90be4cf62 100644 --- a/packages/vue-query/src/__tests__/useIsFetching.test.ts +++ b/packages/vue-query/src/__tests__/useIsFetching.test.ts @@ -89,7 +89,7 @@ describe('useIsFetching', () => { }) test('should unwrap refs arguments', () => { - const key = ref(['key']); + const key = ref(['key']) const filters = ref({ stale: ref(true) }) const result = parseFilterArgs(key, filters) diff --git a/packages/vue-query/src/__tests__/useIsMutating.test.ts b/packages/vue-query/src/__tests__/useIsMutating.test.ts index 111642f78a..afc96654e2 100644 --- a/packages/vue-query/src/__tests__/useIsMutating.test.ts +++ b/packages/vue-query/src/__tests__/useIsMutating.test.ts @@ -95,7 +95,7 @@ describe('useIsMutating', () => { }) test('should unwrap refs arguments', () => { - const key = ref(['key']); + const key = ref(['key']) const filters = ref({ fetching: ref(true) }) const result = parseFilterArgs(key, filters) diff --git a/packages/vue-query/src/__tests__/vueQueryPlugin.test.ts b/packages/vue-query/src/__tests__/vueQueryPlugin.test.ts index d8cc069f6c..7128c4e3ad 100644 --- a/packages/vue-query/src/__tests__/vueQueryPlugin.test.ts +++ b/packages/vue-query/src/__tests__/vueQueryPlugin.test.ts @@ -280,10 +280,9 @@ describe('VueQueryPlugin', () => { expect(customClient.isRestoring.value).toBeTruthy() - await flushPromises(); + await flushPromises() expect(customClient.isRestoring.value).toBeFalsy() - }) }) }) diff --git a/packages/vue-query/src/useBaseQuery.ts b/packages/vue-query/src/useBaseQuery.ts index 5183133a78..8c8ecda658 100644 --- a/packages/vue-query/src/useBaseQuery.ts +++ b/packages/vue-query/src/useBaseQuery.ts @@ -62,9 +62,9 @@ export function useBaseQuery< const defaultedOptions = computed(() => { const defaulted = queryClient.defaultQueryOptions(options.value) defaulted._optimisticResults = queryClient.isRestoring.value - ? 'isRestoring' - : 'optimistic' - + ? 'isRestoring' + : 'optimistic' + return defaulted }) @@ -78,10 +78,8 @@ export function useBaseQuery< watch( queryClient.isRestoring, (isRestoring) => { - // isRestoring.value = val - if (!isRestoring) { - unsubscribe.value(); + unsubscribe.value() unsubscribe.value = observer.subscribe((result) => { updateState(state, result) }) diff --git a/packages/vue-query/src/useIsFetching.ts b/packages/vue-query/src/useIsFetching.ts index 390b19c441..857a27f86f 100644 --- a/packages/vue-query/src/useIsFetching.ts +++ b/packages/vue-query/src/useIsFetching.ts @@ -11,7 +11,7 @@ export type QueryFilters = MaybeRefDeep> export function useIsFetching(filters?: QueryFilters): Ref export function useIsFetching( queryKey?: MaybeRef, - filters?: Omit, + filters?: Omit, ): Ref export function useIsFetching( arg1?: MaybeRef | QueryFilters, diff --git a/packages/vue-query/src/useIsMutating.ts b/packages/vue-query/src/useIsMutating.ts index ed49957172..0d0e497dee 100644 --- a/packages/vue-query/src/useIsMutating.ts +++ b/packages/vue-query/src/useIsMutating.ts @@ -58,4 +58,4 @@ export function parseFilterArgs( } return cloneDeepUnref(options) as WithQueryClientKey -} \ No newline at end of file +} From 19e4e463314ad06abfea964ea9c2b28b8d9cd198 Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Sun, 13 Nov 2022 20:52:21 +0100 Subject: [PATCH 10/15] fix(vue-query): suspense unsubscribe from watch --- packages/vue-query/src/useBaseQuery.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/vue-query/src/useBaseQuery.ts b/packages/vue-query/src/useBaseQuery.ts index 8c8ecda658..74e0f5c54b 100644 --- a/packages/vue-query/src/useBaseQuery.ts +++ b/packages/vue-query/src/useBaseQuery.ts @@ -103,14 +103,19 @@ export function useBaseQuery< const suspense = () => { return new Promise>((resolve) => { + let stopWatch = () => { + //noop + } const run = () => { if (defaultedOptions.value.enabled !== false) { const optimisticResult = observer.getOptimisticResult( defaultedOptions.value, ) if (optimisticResult.isStale) { + stopWatch() resolve(observer.fetchOptimistic(defaultedOptions.value)) } else { + stopWatch() resolve(optimisticResult) } } @@ -118,7 +123,7 @@ export function useBaseQuery< run() - watch(defaultedOptions, run, { deep: true }) + stopWatch = watch(defaultedOptions, run, { deep: true }) }) } From 3dfd9ef09818feef8b360edf667b97fcc33f2c75 Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Sun, 13 Nov 2022 23:54:44 +0100 Subject: [PATCH 11/15] feat: persistent useQueries --- packages/vue-query/src/useQueries.ts | 72 ++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 19 deletions(-) diff --git a/packages/vue-query/src/useQueries.ts b/packages/vue-query/src/useQueries.ts index 7311f61ac1..0f5e5c4696 100644 --- a/packages/vue-query/src/useQueries.ts +++ b/packages/vue-query/src/useQueries.ts @@ -1,13 +1,21 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { QueriesObserver } from '@tanstack/query-core' -import { onScopeDispose, reactive, readonly, watch } from 'vue-demi' +import { + computed, + onScopeDispose, + reactive, + readonly, + ref, + watch, +} from 'vue-demi' import type { Ref } from 'vue-demi' import type { QueryFunction, QueryObserverResult } from '@tanstack/query-core' import { useQueryClient } from './useQueryClient' -import type { UseQueryOptions } from './useQuery' import { cloneDeepUnref } from './utils' +import type { UseQueryOptions } from './useQuery' +import type { QueryClient } from './queryClient' // Avoid TS depth-limit error in case of large array literal type MAXIMUM_DEPTH = 20 @@ -130,37 +138,63 @@ export function useQueries({ }: { queries: Ref> | UseQueriesOptionsArg }): Readonly> { - const unreffedQueries = cloneDeepUnref(queries) as UseQueriesOptionsArg + const options = computed( + () => cloneDeepUnref(queries) as UseQueriesOptionsArg, + ) - const queryClientKey = unreffedQueries[0]?.queryClientKey - const optionsQueryClient = unreffedQueries[0]?.queryClient + const queryClientKey = options.value[0]?.queryClientKey + const optionsQueryClient = options.value[0]?.queryClient as + | QueryClient + | undefined const queryClient = optionsQueryClient ?? useQueryClient(queryClientKey) - const defaultedQueries = unreffedQueries.map((options) => { - return queryClient.defaultQueryOptions(options) - }) + const defaultedQueries = computed(() => + options.value.map((options) => { + const defaulted = queryClient.defaultQueryOptions(options) + defaulted._optimisticResults = queryClient.isRestoring.value + ? 'isRestoring' + : 'optimistic' + + return defaulted + }), + ) - const observer = new QueriesObserver(queryClient, defaultedQueries) + const observer = new QueriesObserver(queryClient, defaultedQueries.value) const state = reactive(observer.getCurrentResult()) - const unsubscribe = observer.subscribe((result) => { - state.splice(0, state.length, ...result) + const unsubscribe = ref(() => { + // noop }) watch( - () => queries, + queryClient.isRestoring, + (isRestoring) => { + if (!isRestoring) { + unsubscribe.value() + unsubscribe.value = observer.subscribe((result) => { + state.splice(0, result.length, ...result) + }) + // Subscription would not fire for persisted results + state.splice( + 0, + state.length, + ...observer.getOptimisticResult(defaultedQueries.value), + ) + } + }, + { immediate: true }, + ) + + watch( + options, () => { - const defaulted = ( - cloneDeepUnref(queries) as UseQueriesOptionsArg - ).map((options) => { - return queryClient.defaultQueryOptions(options) - }) - observer.setQueries(defaulted) + observer.setQueries(defaultedQueries.value) + state.splice(0, state.length, ...observer.getCurrentResult()) }, { deep: true }, ) onScopeDispose(() => { - unsubscribe() + unsubscribe.value() }) return readonly(state) as UseQueriesResults From 771de02341f6c6317cfece87c0bf59e07d80882f Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Mon, 14 Nov 2022 00:02:05 +0100 Subject: [PATCH 12/15] chore: examples formatting --- examples/vue/basic/src/App.vue | 24 ++++++++++----------- examples/vue/basic/src/Post.vue | 24 ++++++++++----------- examples/vue/basic/src/Posts.vue | 26 +++++++++++------------ examples/vue/basic/src/main.ts | 8 +++---- examples/vue/basic/src/shims-vue.d.ts | 8 +++---- examples/vue/basic/src/types.d.ts | 8 +++---- examples/vue/persister/src/App.vue | 24 ++++++++++----------- examples/vue/persister/src/Post.vue | 24 ++++++++++----------- examples/vue/persister/src/Posts.vue | 26 +++++++++++------------ examples/vue/persister/src/main.ts | 7 +++++- examples/vue/persister/src/shims-vue.d.ts | 8 +++---- examples/vue/persister/src/types.d.ts | 8 +++---- package.json | 2 +- 13 files changed, 101 insertions(+), 96 deletions(-) diff --git a/examples/vue/basic/src/App.vue b/examples/vue/basic/src/App.vue index 52028d9dcd..c8275e1840 100644 --- a/examples/vue/basic/src/App.vue +++ b/examples/vue/basic/src/App.vue @@ -1,29 +1,29 @@