From b2ae7633b34fd35dce00d0a7a25c9f1cf744bad3 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Thu, 8 Feb 2024 17:59:58 +0100 Subject: [PATCH] feat: track used params --- .../src/pages/users/colada-loader.[id].vue | 1 + .../defineColadaLoader.spec.ts | 2 +- src/data-fetching_new/defineColadaLoader.ts | 69 +++++++++++++---- src/data-fetching_new/utils.ts | 77 +++++++++++++++++++ 4 files changed, 132 insertions(+), 17 deletions(-) diff --git a/playground/src/pages/users/colada-loader.[id].vue b/playground/src/pages/users/colada-loader.[id].vue index 191c59eae..fbac631df 100644 --- a/playground/src/pages/users/colada-loader.[id].vue +++ b/playground/src/pages/users/colada-loader.[id].vue @@ -35,6 +35,7 @@ export const useUserData = defineColadaLoader('/users/colada-loader.[id]', { console.log('[🍹] key', to.fullPath) return ['loader-users', to.params.id] }, + staleTime: 10000, }) diff --git a/src/data-fetching_new/defineColadaLoader.spec.ts b/src/data-fetching_new/defineColadaLoader.spec.ts index 478ad415b..c73e34c53 100644 --- a/src/data-fetching_new/defineColadaLoader.spec.ts +++ b/src/data-fetching_new/defineColadaLoader.spec.ts @@ -117,7 +117,7 @@ describe( } } - it.todo('avoids refetching fresh data when navigating', async () => { + it('avoids refetching fresh data when navigating', async () => { const query = vi.fn().mockResolvedValue('data') const useData = defineColadaLoader({ query, diff --git a/src/data-fetching_new/defineColadaLoader.ts b/src/data-fetching_new/defineColadaLoader.ts index 4204957e1..1de9280f2 100644 --- a/src/data-fetching_new/defineColadaLoader.ts +++ b/src/data-fetching_new/defineColadaLoader.ts @@ -1,4 +1,4 @@ -import { useRoute, useRouter } from 'vue-router' +import { useRoute, useRouter, type LocationQuery } from 'vue-router' import type { _RouteLocationNormalizedLoaded, _RouteRecordName, @@ -26,7 +26,9 @@ import { IS_CLIENT, assign, getCurrentContext, + isSubsetOf, setCurrentContext, + trackRoute, } from './utils' import { Ref, ShallowRef, ref, shallowRef } from 'vue' import { NavigationResult } from './navigation-guard' @@ -94,6 +96,7 @@ export function defineColadaLoader( DataLoaderColadaEntry > const key = keyText(options.key(to)) + const [trackedRoute, params, query, hash] = trackRoute(to) if (!entries.has(loader)) { const pendingTo = shallowRef<_RouteLocationNormalizedLoaded>(to) entries.set(loader, { @@ -113,6 +116,7 @@ export function defineColadaLoader( // @ts-expect-error: FIXME: once pendingTo is removed from DataLoaderEntryBase commit, + tracked: new Map(), ext: null, pendingTo, @@ -133,16 +137,25 @@ export function defineColadaLoader( entry.ext = useQuery({ ...options, // FIXME: type Promise instead of Promise - query: () => - // TODO: run within app context? - loader(entry.pendingTo.value, { - signal: entry.pendingTo.value.meta[ABORT_CONTROLLER_KEY]!.signal, - }), + query: () => { + const route = entry.pendingTo.value + const [trackedRoute, params, query, hash] = trackRoute(route) + entry.tracked.set(options.key(trackedRoute).join('|'), { + ready: false, + params, + query, + hash, + }) + + return loader(trackedRoute, { + signal: route.meta[ABORT_CONTROLLER_KEY]!.signal, + }) + }, key: () => options.key(entry.pendingTo.value), }) } - const { error, isLoading, data, ext } = entry + const { isLoading, data, ext } = entry // we are rendering for the first time and we have initial data // we need to synchronously set the value so it's available in components @@ -163,8 +176,9 @@ export function defineColadaLoader( // TODO: test entry.pendingTo.value.meta[ABORT_CONTROLLER_KEY]!.abort() // ensure we call refetch instead of refresh - // TODO: only if to is different from the pendintTo **consumed** properties - reload = true + // TODO: only if to is different from the pendingTo **consumed** properties + const tracked = entry.tracked.get(key.join('|')) + reload = !tracked || hasRouteChanged(to, tracked) } // Currently load for this loader @@ -187,13 +201,14 @@ export function defineColadaLoader( const currentLoad = ext[reload ? 'refetch' : 'refresh']() .then((d) => { - console.log( - `✅ resolved ${key}`, - to.fullPath, - `accepted: ${ - entry.pendingLoad === currentLoad - }; data:\n${JSON.stringify(d)}\n${JSON.stringify(ext.data.value)}` - ) + // console.log( + // `✅ resolved ${key}`, + // to.fullPath, + // `accepted: ${ + // entry.pendingLoad === currentLoad + // }; data:\n${JSON.stringify(d)}\n${JSON.stringify(ext.data.value)}` + // ) + console.log(`👀 Tracked stuff`, entry.tracked) if (entry.pendingLoad === currentLoad) { // propagate the error if (ext.error.value) { @@ -264,6 +279,7 @@ export function defineColadaLoader( to.meta[NAVIGATION_RESULTS_KEY]!.push(this.staged) } else { this.data.value = this.staged + this.tracked.get(key.join('|'))!.ready = true } } // The navigation was changed so avoid resetting the error @@ -432,12 +448,33 @@ export interface DataLoaderColadaEntry pendingTo: ShallowRef<_RouteLocationNormalizedLoaded> _pendingTo: _RouteLocationNormalizedLoaded | null + tracked: Map + /** * Extended options for pinia colada */ ext: UseQueryReturn | null } +interface TrackedRoute { + ready: boolean + params: Partial + query: Partial + hash: { v: string | null } +} + +function hasRouteChanged( + to: _RouteLocationNormalizedLoaded, + tracked: TrackedRoute +): boolean { + return ( + !tracked.ready || + !isSubsetOf(tracked.params, to.params) || + !isSubsetOf(tracked.query, to.query) || + (tracked.hash.v != null && tracked.hash.v !== to.hash) + ) +} + const DEFAULT_DEFINE_LOADER_OPTIONS = { lazy: false, server: true, diff --git a/src/data-fetching_new/utils.ts b/src/data-fetching_new/utils.ts index 27e4781a6..a273c054b 100644 --- a/src/data-fetching_new/utils.ts +++ b/src/data-fetching_new/utils.ts @@ -2,6 +2,7 @@ import type { DataLoaderEntryBase, UseDataLoader } from './createDataLoader' import { IS_USE_DATA_LOADER_KEY } from './meta-extensions' import { type _Router } from '../type-extensions/router' import { type _RouteLocationNormalizedLoaded } from '../type-extensions/routeLocation' +import { type LocationQuery } from 'vue-router' /** * Check if a value is a `DataLoader`. @@ -57,3 +58,79 @@ export const assign = Object.assign * @internal */ export type _MaybePromise = T | Promise + +/** + * Track the reads of a route and its properties + * @internal + * @param route - route to track + */ +export function trackRoute(route: _RouteLocationNormalizedLoaded) { + const [params, paramReads] = trackObjectReads(route.params) + const [query, queryReads] = trackObjectReads(route.query) + let hash: { v: string | null } = { v: null } + return [ + { + ...route, + // track the hash + get hash() { + return (hash.v = route.hash) + }, + params, + query, + }, + paramReads, + queryReads, + hash, + ] as const +} + +/** + * Track the reads of an object (that doesn't change) and add the read properties to an object + * @internal + * @param obj - object to track + */ +function trackObjectReads>(obj: T) { + const reads: Partial = {} + return [ + new Proxy(obj, { + get(target, p: Extract, receiver) { + const value = Reflect.get(target, p, receiver) + reads[p] = value + return value + }, + }), + reads, + ] as const +} + +/** + * Returns `true` if `inner` is a subset of `outer`. Used to check if a tr + * + * @internal + * @param outer - the bigger params + * @param inner - the smaller params + */ +export function isSubsetOf( + inner: Partial, + outer: LocationQuery +): boolean { + for (const key in inner) { + const innerValue = inner[key] + const outerValue = outer[key] + if (typeof innerValue === 'string') { + if (innerValue !== outerValue) return false + } else if (!innerValue || !outerValue) { + // if one of them is undefined, we need to check if the other is undefined too + if (innerValue !== outerValue) return false + } else { + if ( + !Array.isArray(outerValue) || + outerValue.length !== innerValue.length || + innerValue.some((value, i) => value !== outerValue[i]) + ) + return false + } + } + + return true +}