Replies: 2 comments 1 reply
-
|
I’m also very interested in a tanstack query loader. Tanstack query is often used extensively in existing projects where it is not feasible to migrate to pinia colada. @ZachHaber it would be great if you could share what you are doing at the moment and what the issues are. Maybe we can use it to drive implementation of an official loader? |
Beta Was this translation helpful? Give feedback.
-
|
@yakobe, here's my implementation. As I mentioned I basically took the const currentLoad = (
!reload && ext!.isSuccess.value && !ext!.isStale.value
? Promise.resolve()
: ext!.refetch({ cancelRefetch: reload })
)I have tested it with throwing Here's an example of using it: export const useAppData = defineQueryLoader('App', {
queryKey: (to) => {
const appId = +to.params.appId;
return ['app', Number.isNaN(appId) ? to.params.appId : appId];
},
async query(to, { signal }) {
try {
return await api.get(`app/${+to.params.appId}`, { signal }).json<App>();
} catch (error) {
if (error instanceof HTTPError) {
if (error.response.status === 403) {
throw new NavigationResult({
name: 'App Request',
params: { appId: to.params.appId },
query: { env: to.query.env },
});
} else if (error.response.status === 404) {
throw new NavigationResult({ name: 'Home' });
}
}
throw error;
}
},
});defineQueryLoader.tsimport {
type QueryKey,
type QueryStatus,
type UseQueryDefinedReturnType,
type UseQueryOptions,
type UseQueryReturnType,
useQuery,
} from '@tanstack/vue-query';
import {
type _DefineLoaderEntryMap,
type _PromiseMerged,
ABORT_CONTROLLER_KEY,
APP_KEY,
assign,
type DataLoaderContextBase,
type DataLoaderEntryBase,
type DefineDataLoaderOptionsBase_DefinedData,
type DefineDataLoaderOptionsBase_LaxData,
type DefineLoaderFn,
type ErrorDefault,
getCurrentContext,
IS_SSR_KEY,
IS_USE_DATA_LOADER_KEY,
isSubsetOf,
LOADER_ENTRIES_KEY,
NAVIGATION_RESULTS_KEY,
NavigationResult,
PENDING_LOCATION_KEY,
STAGED_NO_VALUE,
setCurrentContext,
toLazyValue,
trackRoute,
type UseDataLoader,
type UseDataLoaderResult,
} from 'unplugin-vue-router/data-loaders';
import {
type ComputedRef,
computed,
type ShallowRef,
shallowRef,
type UnwrapRef,
watch,
} from 'vue';
import {
type LocationQuery,
type RouteLocationNormalizedLoaded,
type RouteMap,
type Router,
useRoute,
useRouter,
} from 'vue-router';
/**
* Creates a Vue Query data loader with `data` is always defined.
*
* @param name - name of the route to have typed routes
* @param options - options to configure the data loader
*/
export function defineQueryLoader<Name extends keyof RouteMap, Data>(
name: Name,
options: DefineDataQueryLoaderOptions_DefinedData<Name, Data>,
): UseDataLoaderQuery_DefinedData<Data>;
/**
* Creates a Vue Query data loader with `data` is possibly `undefined`.
*
* @param name - name of the route to have typed routes
* @param options - options to configure the data loader
*/
export function defineQueryLoader<Name extends keyof RouteMap, Data>(
name: Name,
options: DefineDataQueryLoaderOptions_LaxData<Name, Data>,
): UseDataLoaderQuery_LaxData<Data>;
/**
* Creates a Vue Query data loader with `data` is always defined.
* @param options - options to configure the data loader
*/
export function defineQueryLoader<Data>(
options: DefineDataQueryLoaderOptions_DefinedData<keyof RouteMap, Data>,
): UseDataLoaderQuery_DefinedData<Data>;
/**
* Creates a Vue Query data loader with `data` is possibly `undefined`.
* @param options - options to configure the data loader
*/
export function defineQueryLoader<Data>(
options: DefineDataQueryLoaderOptions_LaxData<keyof RouteMap, Data>,
): UseDataLoaderQuery_LaxData<Data>;
export function defineQueryLoader<Data>(
nameOrOptions:
| keyof RouteMap
| DefineDataQueryLoaderOptions_LaxData<keyof RouteMap, Data>,
_options?: DefineDataQueryLoaderOptions_LaxData<keyof RouteMap, Data>,
): UseDataLoaderQuery_LaxData<Data> {
// TODO: make it DEV only and remove the first argument in production mode
// resolve option overrides
// biome-ignore lint/style/noParameterAssign: explanation
_options =
_options ||
(nameOrOptions as DefineDataQueryLoaderOptions_LaxData<
keyof RouteMap,
Data
>);
const loader = _options.query;
const options = {
...DEFAULT_DEFINE_LOADER_OPTIONS,
..._options,
commit: _options?.commit || 'after-load',
} as DefineDataQueryLoaderOptions_LaxData<keyof RouteMap, Data>;
let isInitial = true;
function load(
to: RouteLocationNormalizedLoaded,
router: Router,
from?: RouteLocationNormalizedLoaded,
parent?: DataLoaderEntryBase,
reload?: boolean,
): Promise<void> {
const entries = router[LOADER_ENTRIES_KEY]! as _DefineLoaderEntryMap<
DataLoaderQueryEntry<unknown>
>;
const isSSR = router[IS_SSR_KEY];
const key = serializeQueryKey(options.queryKey, to);
if (!entries.has(loader)) {
const route = shallowRef<RouteLocationNormalizedLoaded>(to);
entries.set(loader, {
// force the type to match
data: shallowRef<Data | undefined>(),
isLoading: shallowRef(false),
// biome-ignore lint/suspicious/noExplicitAny: explanation
error: shallowRef<any>(),
to,
options,
children: new Set(),
resetPending() {
this.pendingTo = null;
this.pendingLoad = null;
this.isLoading.value = false;
},
staged: STAGED_NO_VALUE,
stagedError: null,
commit,
tracked: new Map(),
ext: null,
route,
pendingTo: null,
pendingLoad: null,
});
}
const entry = entries.get(loader)!;
// Nested loaders might get called before the navigation guard calls them, so we need to manually skip these calls
if (entry.pendingTo === to && entry.pendingLoad) {
// console.log(`🔁 already loading "${key}"`)
return entry.pendingLoad;
}
// save the current context to restore it later
const currentContext = getCurrentContext();
if (import.meta.env.DEV) {
if (parent !== currentContext[0]) {
console.warn(
`❌👶 "${key}" has a different parent than the current context. This shouldn't be happening. Please report a bug with a reproduction to https://github.com/posva/unplugin-vue-router/`,
);
}
}
// set the current context before loading so nested loaders can use it
setCurrentContext([entry, router, to]);
const app = router[APP_KEY];
if (!entry.ext) {
// console.log(`🚀 creating query for "${key}"`)
// @ts-expect-error This needs to be fixed eventually
entry.ext = useQuery({
...options,
queryFn: (): Promise<Data> => {
const route = entry.route.value;
const [trackedRoute, params, query, hash] = trackRoute(route);
entry.tracked.set(
joinKeys(
serializeQueryKey(
options.queryKey,
trackedRoute as RouteLocationNormalizedLoaded<keyof RouteMap>,
),
),
{
ready: false,
params,
query,
hash,
},
);
// needed for automatic refetching and nested loaders
// https://github.com/posva/unplugin-vue-router/issues/583
return app.runWithContext(() =>
loader(
trackedRoute as RouteLocationNormalizedLoaded<keyof RouteMap>,
{
signal: route.meta[ABORT_CONTROLLER_KEY]?.signal,
},
),
);
},
queryKey: computed(() =>
toValueWithParameters(options.queryKey, entry.route.value),
),
// TODO: cleanup if gc
// onDestroy() {
// entries.delete(loader)
// }
});
// avoid double reload since calling `useQuery()` will trigger a refresh
// and we might also do it below for nested loaders
if (entry.ext!.fetchStatus.value === 'fetching') {
// biome-ignore lint/style/noParameterAssign: explanation
reload = false;
}
}
// TODO: should also reload in the case of nested loaders if a nested loader has been invalidated
const { isLoading, data, error, 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
// even if it's not exported
if (isInitial) {
isInitial = false;
if (ext!.data.value !== undefined) {
data.value = ext!.data.value;
// restore the context like in the finally branch
// otherwise we might end up with entry === parentEntry
// TODO: add test that checks the currentContext is reset
setCurrentContext(
currentContext as Parameters<typeof setCurrentContext>[0],
);
// pendingLoad is set for guards to work
// biome-ignore lint/suspicious/noAssignInExpressions: explanation
return (entry.pendingLoad = Promise.resolve());
}
}
// console.log(
// `😎 Loading context to "${to.fullPath}" with current "${currentContext[2]?.fullPath}"`
// )
if (entry.route.value !== to) {
// ensure we call refetch instead of refresh
const tracked = entry.tracked.get(joinKeys(key));
// biome-ignore lint/style/noParameterAssign: explanation
reload = !tracked || hasRouteChanged(to, tracked);
}
// Currently load for this loader
entry.route.value = entry.pendingTo = to;
isLoading.value = true;
entry.staged = STAGED_NO_VALUE;
// preserve error until data is committed
entry.stagedError = error.value;
const currentLoad = (
!reload && ext!.isSuccess.value && !ext!.isStale.value
? Promise.resolve()
: ext!.refetch({ cancelRefetch: reload })
)
.then(() => {
// console.log(
// `✅ resolved ${key}`,
// to.fullPath,
// `accepted: ${
// entry.pendingLoad === currentLoad
// }; data:\n${JSON.stringify(d)}\n${JSON.stringify(ext.data.value)}`
// )
if (entry.pendingLoad === currentLoad) {
const newError = ext!.error.value;
// propagate the error
if (newError) {
// console.log(
// '‼️ rejected',
// to.fullPath,
// `accepted: ${entry.pendingLoad === currentLoad} =`,
// e
// )
// in this case, commit will never be called so we should just drop the error
entry.stagedError = newError;
// propagate error if non lazy or during SSR
// NOTE: Cannot be handled at the guard level because of nested loaders
if (!toLazyValue(options.lazy, to, from) || isSSR) {
throw newError;
}
} else {
// let the navigation guard collect the result
const newData = ext!.data.value;
if (newData instanceof NavigationResult) {
to.meta[NAVIGATION_RESULTS_KEY]!.push(newData);
} else {
entry.staged = newData;
entry.stagedError = null;
}
}
}
// else if (newError) {
// TODO: Figure out cases where this was needed and test it
// console.log(`❌ Discarded old result for "${key}"`, d, ext.data.value)
// ext.data.value = data.value
// ext.error.value = error.value
// }
})
.finally(() => {
// restore the context after the promise resolves
// so that nested loaders can be aware of their parent
// when multiple loaders are nested and awaited one after the other
// within a loader
setCurrentContext(
currentContext as Parameters<typeof setCurrentContext>[0],
);
// console.log(
// `😩 restored context ${key}`,
// currentContext?.[2]?.fullPath
// )
if (entry.pendingLoad === currentLoad) {
isLoading.value = false;
// we must run commit here so nested loaders are ready before used by their parents
if (
options.commit === 'immediate' ||
// outside of a navigation
!router[PENDING_LOCATION_KEY]
) {
entry.commit(to);
}
} else {
// For debugging purposes and refactoring the code
// console.log(
// to.meta[ABORT_CONTROLLER_KEY]!.signal.aborted ? '✅' : '❌'
// )
}
});
// restore the context after the first tick to avoid lazy loaders to use their own context as parent
setCurrentContext(
currentContext as Parameters<typeof setCurrentContext>[0],
);
// this still runs before the promise resolves even if loader is sync
entry.pendingLoad = currentLoad;
return currentLoad;
}
function commit(
this: DataLoaderQueryEntry<Data>,
to: RouteLocationNormalizedLoaded,
) {
const key = serializeQueryKey(options.queryKey, to);
// console.log(`👉 commit "${key}"`)
if (this.pendingTo === to) {
// console.log(' ->', this.staged)
if (import.meta.env.DEV) {
if (this.staged === STAGED_NO_VALUE) {
console.warn(
`Loader "${key}"'s "commit()" was called but there is no staged data.`,
);
}
}
// if the entry is null, it means the loader never resolved, maybe there was an error
if (this.staged !== STAGED_NO_VALUE) {
this.data.value = this.staged;
if (import.meta.env.DEV && !this.tracked.has(joinKeys(key))) {
console.warn(
`A query was defined with the same key as the loader "[${key.join(', ')}]". If the "key" is meant to be the same, you should directly use the data loader instead. If not, change the key of the "useQuery()".`,
);
// avoid a crash that requires the page to be reloaded
return;
}
this.tracked.get(joinKeys(key))!.ready = true;
}
// we always commit the error unless the navigation was cancelled
this.error.value = this.stagedError;
// reset the staged values so they can't be commit
this.staged = STAGED_NO_VALUE;
// preserve error until data is committed
this.stagedError = this.error.value;
this.to = to;
this.pendingTo = null;
// FIXME: move pendingLoad to currentLoad or use `to` to check if the current version is valid
// we intentionally keep pendingLoad so it can be reused until the navigation is finished
// children entries cannot be committed from the navigation guard, so the parent must tell them
for (const childEntry of this.children) {
childEntry.commit(to);
}
} else {
// console.log(` -> skipped`)
}
}
// @ts-expect-error: requires the internals and symbol that are added later
const useDataLoader: // for ts
UseDataLoaderQuery_LaxData<Data> = () => {
// work with nested data loaders
const currentEntry = getCurrentContext();
// TODO: should _route also contain from?
const [parentEntry, _router, _route] = currentEntry;
// fallback to the global router and routes for useDataLoaders used within components
const router = _router || useRouter();
const route = _route || (useRoute() as RouteLocationNormalizedLoaded);
const app = router[APP_KEY];
const entries = router[
LOADER_ENTRIES_KEY
]! as unknown as _DefineLoaderEntryMap<DataLoaderQueryEntry<unknown>>;
let entry = entries.get(loader) as
| DataLoaderQueryEntry<Data, ErrorDefault>
| undefined;
if (
// if the entry doesn't exist, create it with load and ensure it's loading
!entry ||
// we are nested and the parent is loading a different route than us
(parentEntry && entry.pendingTo !== route) ||
// The user somehow rendered the page without a navigation
!entry.pendingLoad
) {
// console.log(
// `🔁 loading from useData for "${options.key}": "${route.fullPath}"`
// )
app.runWithContext(() =>
// in this case we always need to run the functions for nested loaders consistency
load(
route as RouteLocationNormalizedLoaded,
router,
undefined,
parentEntry,
true,
),
);
}
entry = entries.get(loader)! as DataLoaderQueryEntry<Data, ErrorDefault>;
// add ourselves to the parent entry children
if (parentEntry) {
if (parentEntry !== entry) {
// console.log(`👶 "${options.key}" has parent ${parentEntry}`)
parentEntry.children.add(entry!);
} else {
// console.warn(
// `👶❌ "${options.key}" has itself as parent. This shouldn't be happening. Please report a bug with a reproduction to https://github.com/posva/unplugin-vue-router/`
// )
}
}
const { data, error, isLoading, ext } = entry;
// TODO: add watchers only once alongside the entry
// update the data when Vue Query updates it e.g. after visibility change
watch(ext!.data, (newData) => {
// only if we are not in the middle of a navigation
if (!router[PENDING_LOCATION_KEY]) {
data.value = newData;
}
});
watch(ext!.isLoading, (isFetching) => {
if (!router[PENDING_LOCATION_KEY]) {
isLoading.value = isFetching;
}
});
watch(ext!.error, (newError) => {
if (!router[PENDING_LOCATION_KEY]) {
error.value = newError;
}
});
const useDataLoaderResult = {
data,
error,
isLoading,
reload: (to: RouteLocationNormalizedLoaded = router.currentRoute.value) =>
app
.runWithContext(() => load(to, router, undefined, undefined, true))
.then(() => entry!.commit(to)),
// Vue Query
// @ts-expect-error This needs to be fixed eventually
refetch: (
to: RouteLocationNormalizedLoaded = router.currentRoute.value,
) =>
app
.runWithContext(() => load(to, router, undefined, undefined, true))
// biome-ignore lint/complexity/noCommaOperator: this is how it was set up
.then(() => (entry!.commit(to), entry!.ext!)),
// @ts-expect-error This needs to be fixed eventually
refresh: (
to: RouteLocationNormalizedLoaded = router.currentRoute.value,
) =>
app
.runWithContext(() => load(to, router))
// biome-ignore lint/complexity/noCommaOperator: this is how it was set up
.then(() => (entry!.commit(to), entry.ext!)),
status: ext!.status,
asyncStatus: ext!.fetchStatus,
state: computed(() => ({
data: ext!.data.value,
error: ext!.error.value,
status: ext!.status.value,
})),
isPending: ext!.isPending,
} satisfies UseDataLoaderQueryResult<Data | undefined>;
// load ensures there is a pending load
const promise = entry
.pendingLoad!.then(() => {
// nested loaders might wait for all loaders to be ready before setting data
// so we need to return the staged value if it exists as it will be the latest one
return entry!.staged === STAGED_NO_VALUE
? ext!.data.value
: entry!.staged;
})
// we only want the error if we are nesting the loader
// otherwise this will end up in "Unhandled promise rejection"
.catch((e) => (parentEntry ? Promise.reject(e) : null));
// Restore the context to avoid sequential calls to be nested
setCurrentContext(currentEntry as Parameters<typeof setCurrentContext>[0]);
return assign(promise, useDataLoaderResult);
};
// mark it as a data loader
useDataLoader[IS_USE_DATA_LOADER_KEY] = true;
// add the internals
useDataLoader._ = {
load,
options,
// @ts-expect-error: return type has the generics
getEntry(router: Router) {
return router[LOADER_ENTRIES_KEY]!.get(loader)!;
},
};
return useDataLoader;
}
export const joinKeys = (keys: string[]): string => keys.join('|');
/**
* Base type with docs for the options of `defineQueryLoader`.
* @internal
*/
export interface _DefineDataQueryLoaderOptions_Common<
Name extends keyof RouteMap,
Data,
> extends Omit<
UnwrapRef<UseQueryOptions<Data, ErrorDefault, Data>>,
'queryFn' | 'queryKey' | 'placeholderData' | 'initialData'
> {
/**
* Key associated with the data and passed to Vue Query
* @param to - Route to load the data
*/
queryKey: QueryKey | ((to: RouteLocationNormalizedLoaded<Name>) => QueryKey);
/**
* Function that returns a promise with the data.
*/
query: DefineLoaderFn<
Data,
DataQueryLoaderContext,
RouteLocationNormalizedLoaded<Name>
>;
//
// TODO: option to skip refresh if the used properties of the route haven't changed
}
/**
* Options for `defineQueryLoader` when the data is possibly `undefined`.
*/
export interface DefineDataQueryLoaderOptions_LaxData<
Name extends keyof RouteMap,
Data,
> extends _DefineDataQueryLoaderOptions_Common<Name, Data>,
DefineDataLoaderOptionsBase_LaxData {}
/**
* Options for `defineQueryLoader` when the data is always defined.
*/
export interface DefineDataQueryLoaderOptions_DefinedData<
Name extends keyof RouteMap,
Data,
> extends _DefineDataQueryLoaderOptions_Common<Name, Data>,
DefineDataLoaderOptionsBase_DefinedData {}
/**
* @deprecated Use {@link DefineDataQueryLoaderOptions_LaxData} instead.
*/
export type DefineDataQueryLoaderOptions<
Name extends keyof RouteMap,
Data,
> = DefineDataQueryLoaderOptions_LaxData<Name, Data>;
/**
* @internal
*/
export interface DataQueryLoaderContext extends DataLoaderContextBase {}
export type QueryHookResult<TData, TError> = UseQueryReturnType<TData, TError>;
export type DefinedQueryHookResult<TData, TError> = UseQueryDefinedReturnType<
TData,
TError
>;
type UseQueryReturn<
TData,
TError,
TDataInitial extends TData | undefined,
> = TDataInitial extends TData
? DefinedQueryHookResult<TData, TError>
: QueryHookResult<TData, TError>;
export interface UseDataLoaderQueryResult<
TData,
TError = ErrorDefault,
TDataInitial extends TData | undefined = TData | undefined,
> extends UseDataLoaderResult<TData | TDataInitial, ErrorDefault>,
Pick<
UseQueryReturn<TData, TError, TDataInitial>,
// 'isPending' | 'status' | 'fetchStatus' | 'state'
'fetchStatus' | 'isPending' | 'status'
> {
refetch: (
to?: RouteLocationNormalizedLoaded,
// TODO: we might need to add this in the future
// ...queryArgs: Parameters<UseQueryReturn<Data, any>['refresh']>
) => ReturnType<UseQueryReturn<TData, TError, TDataInitial>['refetch']>;
refresh: (
to?: RouteLocationNormalizedLoaded,
) => ReturnType<UseQueryReturn<TData, TError, TDataInitial>['refetch']>;
state: ComputedRef<{
data: TData | undefined;
error: TError | null;
status: QueryStatus;
}>;
}
/**
* Data Loader composable returned by `defineQueryLoader()`.
*/
export interface UseDataLoaderQuery_LaxData<Data>
extends UseDataLoader<Data | undefined, ErrorDefault> {
/**
* Data Loader composable returned by `defineQueryLoader()`.
*
* @example
* Returns the Data loader data, isLoading, error etc. Meant to be used in `setup()` or `<script setup>` **without `await`**:
* ```vue
* <script setup>
* const { data, isLoading, error } = useUserData()
* </script>
* ```
*
* @example
* It also returns a promise of the data when used in nested loaders. Note this `data` is **not a ref**. This is not meant to be used in `setup()` or `<script setup>`.
* ```ts
* export const useUserConnections = defineLoader(async () => {
* const user = await useUserData()
* return fetchUserConnections(user.id)
* })
* ```
*/
(): _PromiseMerged<
// we can await the raw data
// excluding NavigationResult allows to ignore it in the type of Data when doing
// `return new NavigationResult()` in the loader
Exclude<Data, NavigationResult | undefined>,
// or use it as a composable
UseDataLoaderQueryResult<Exclude<Data, NavigationResult> | undefined>
>;
}
/**
* Data Loader composable returned by `defineQueryLoader()`.
*/
export interface UseDataLoaderQuery_DefinedData<TData>
extends UseDataLoader<TData, ErrorDefault> {
/**
* Data Loader composable returned by `defineQueryLoader()`.
*
* @example
* Returns the Data loader data, isLoading, error etc. Meant to be used in `setup()` or `<script setup>` **without `await`**:
* ```vue
* <script setup>
* const { data, isLoading, error } = useUserData()
* </script>
* ```
*
* @example
* It also returns a promise of the data when used in nested loaders. Note this `data` is **not a ref**. This is not meant to be used in `setup()` or `<script setup>`.
* ```ts
* export const useUserConnections = defineLoader(async () => {
* const user = await useUserData()
* return fetchUserConnections(user.id)
* })
* ```
*/
(): _PromiseMerged<
// we can await the raw data
// excluding NavigationResult allows to ignore it in the type of Data when doing
// `return new NavigationResult()` in the loader
Exclude<TData, NavigationResult | undefined>,
// or use it as a composable
UseDataLoaderQueryResult<
Exclude<TData, NavigationResult>,
ErrorDefault,
Exclude<TData, NavigationResult>
>
>;
}
export interface DataLoaderQueryEntry<
TData,
TError = unknown,
TDataInitial extends TData | undefined = TData | undefined,
> extends DataLoaderEntryBase<TData, TError, TDataInitial> {
/**
* Reactive route passed to Vue Query so it automatically refetch
*/
route: ShallowRef<RouteLocationNormalizedLoaded>;
/**
* Tracked routes to know when the data should be refreshed. Key is the key of the query.
*/
tracked: Map<string, TrackedRoute>;
/**
* Extended options for Vue Query
*/
ext: UseQueryReturn<TData, TError, TDataInitial> | null;
}
interface TrackedRoute {
ready: boolean;
params: Partial<LocationQuery>;
query: Partial<LocationQuery>;
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,
commit: 'after-load',
} satisfies Omit<
DefineDataQueryLoaderOptions_LaxData<keyof RouteMap, unknown>,
'queryKey' | 'query'
>;
const toValueWithParameters = <T, Arg>(
optionValue: T | ((arg: Arg) => T),
arg: Arg,
): T => {
return typeof optionValue === 'function'
? // This should work in TS without a cast
(optionValue as (arg: Arg) => T)(arg)
: optionValue;
};
/**
* Transform the key to a string array so it can be used as a key in caches.
*
* @param key - key to transform
* @param to - route to use
*/
function serializeQueryKey<Name extends keyof RouteMap = keyof RouteMap>(
keyOption: DefineDataQueryLoaderOptions_LaxData<Name, unknown>['queryKey'],
to: RouteLocationNormalizedLoaded,
): string[] {
// biome-ignore lint/suspicious/noExplicitAny: explanation
const key = toValueWithParameters(keyOption, to as any);
const keys = Array.isArray(key) ? key : [key];
return keys.map(stringifyFlatObject);
}
// TODO: import from vue query?
export function stringifyFlatObject(obj: unknown): string {
return obj && typeof obj === 'object'
? JSON.stringify(obj, Object.keys(obj).sort())
: String(obj);
} |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
I saw that there's an incomplete tanstack query/vue query data loader in the data-loaders packages. I was wondering when that is expected to be completed?
I've modified the colada loader to working with tanstack query successfully a while ago, and I can share that if it helps. Though there's still a few issues I haven't figured out how to resolve yet. The biggest being that the query re-runs fully even if there is cached data on route change.
Beta Was this translation helpful? Give feedback.
All reactions