Hooks for managing, caching and syncing asynchronous and remote data in Vue
—
Type-safe helper functions for creating query and infinite query options, providing enhanced TypeScript inference and reusable query configurations.
Helper function for creating type-safe query options with enhanced TypeScript inference.
/**
* Create type-safe query options with enhanced TypeScript inference
* @param options - Query options with defined initial data
* @returns Enhanced query options with type information
*/
function queryOptions<TQueryFnData, TError, TData, TQueryKey>(
options: DefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey>
): DefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey> & {
queryKey: DataTag<TQueryKey, TQueryFnData, TError>;
};
/**
* Create type-safe query options with enhanced TypeScript inference
* @param options - Query options with undefined initial data
* @returns Enhanced query options with type information
*/
function queryOptions<TQueryFnData, TError, TData, TQueryKey>(
options: UndefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey>
): UndefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey> & {
queryKey: DataTag<TQueryKey, TQueryFnData, TError>;
};
// Base implementation for runtime usage
function queryOptions(options: unknown): unknown;Usage Examples:
import { queryOptions, useQuery } from '@tanstack/vue-query';
// Basic query options
const todosQuery = queryOptions({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(res => res.json()),
staleTime: 1000 * 60 * 5, // 5 minutes
});
// Use with useQuery - full type inference
const { data: todos } = useQuery(todosQuery);
// Query options with parameters
const userQuery = (userId: number) => queryOptions({
queryKey: ['user', userId],
queryFn: ({ queryKey }) =>
fetch(`/api/users/${queryKey[1]}`).then(res => res.json()),
enabled: !!userId,
});
// Use with dynamic parameters
const userId = ref(1);
const { data: user } = useQuery(userQuery(userId.value));
// Query options with data transformation
const postsQuery = queryOptions({
queryKey: ['posts'],
queryFn: () => fetch('/api/posts').then(res => res.json()),
select: (posts) => posts.filter(post => post.published),
staleTime: 1000 * 60 * 10,
});
// Query options with initial data
const cachedUserQuery = (userId: number, initialData?: User) => queryOptions({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json()),
initialData,
staleTime: initialData ? 0 : 1000 * 60 * 5,
});
// Reusable query configurations
const createApiQuery = <T>(endpoint: string) => queryOptions({
queryKey: ['api', endpoint],
queryFn: () => fetch(`/api/${endpoint}`).then(res => res.json()) as Promise<T>,
retry: 3,
staleTime: 1000 * 60 * 5,
});
// Use reusable configuration
const usersQuery = createApiQuery<User[]>('users');
const productsQuery = createApiQuery<Product[]>('products');
// Complex query with multiple options
const dashboardQuery = queryOptions({
queryKey: ['dashboard', 'summary'],
queryFn: async () => {
const [users, posts, analytics] = await Promise.all([
fetch('/api/users/count').then(r => r.json()),
fetch('/api/posts/recent').then(r => r.json()),
fetch('/api/analytics/summary').then(r => r.json()),
]);
return { users, posts, analytics };
},
select: (data) => ({
userCount: data.users.total,
recentPosts: data.posts.slice(0, 5),
pageViews: data.analytics.pageViews,
conversionRate: data.analytics.conversions / data.analytics.visits,
}),
staleTime: 1000 * 60 * 10,
refetchInterval: 1000 * 30, // Refresh every 30 seconds
meta: { critical: true },
});
// Conditional query options
const conditionalQuery = (enabled: boolean) => queryOptions({
queryKey: ['conditional-data'],
queryFn: () => fetch('/api/data').then(res => res.json()),
enabled,
retry: enabled ? 3 : 0,
});Helper function for creating type-safe infinite query options with enhanced TypeScript inference.
/**
* Create type-safe infinite query options with enhanced TypeScript inference
* @param options - Infinite query options with undefined initial data
* @returns Enhanced infinite query options with type information
*/
function infiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>(
options: UndefinedInitialDataInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>
): UndefinedInitialDataInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> & {
queryKey: DataTag<TQueryKey, InfiniteData<TQueryFnData>, TError>;
};
/**
* Create type-safe infinite query options with enhanced TypeScript inference
* @param options - Infinite query options with defined initial data
* @returns Enhanced infinite query options with type information
*/
function infiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>(
options: DefinedInitialDataInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>
): DefinedInitialDataInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> & {
queryKey: DataTag<TQueryKey, InfiniteData<TQueryFnData>, TError>;
};
// Base implementation for runtime usage
function infiniteQueryOptions(options: unknown): unknown;Usage Examples:
import { infiniteQueryOptions, useInfiniteQuery } from '@tanstack/vue-query';
// Basic infinite query options
const postsInfiniteQuery = infiniteQueryOptions({
queryKey: ['posts', 'infinite'],
queryFn: ({ pageParam = 1 }) =>
fetch(`/api/posts?page=${pageParam}&limit=10`).then(res => res.json()),
initialPageParam: 1,
getNextPageParam: (lastPage, allPages) =>
lastPage.hasMore ? allPages.length + 1 : undefined,
staleTime: 1000 * 60 * 5,
});
// Use with useInfiniteQuery
const {
data: posts,
fetchNextPage,
hasNextPage,
isFetchingNextPage
} = useInfiniteQuery(postsInfiniteQuery);
// Infinite query with search parameters
const searchInfiniteQuery = (searchTerm: string) => infiniteQueryOptions({
queryKey: ['search', searchTerm, 'infinite'],
queryFn: ({ queryKey, pageParam = 0 }) =>
fetch(`/api/search?q=${queryKey[1]}&offset=${pageParam}&limit=20`)
.then(res => res.json()),
initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam) =>
lastPage.results.length === 20 ? lastPageParam + 20 : undefined,
getPreviousPageParam: (firstPage, allPages, firstPageParam) =>
firstPageParam > 0 ? Math.max(0, firstPageParam - 20) : undefined,
enabled: !!searchTerm && searchTerm.length > 2,
staleTime: 1000 * 60 * 2,
});
// Use with reactive search term
const searchTerm = ref('');
const searchResults = useInfiniteQuery(
computed(() => searchInfiniteQuery(searchTerm.value))
);
// Infinite query with cursor-based pagination
const cursorInfiniteQuery = infiniteQueryOptions({
queryKey: ['timeline'],
queryFn: ({ pageParam }) =>
fetch(`/api/timeline${pageParam ? `?cursor=${pageParam}` : ''}`)
.then(res => res.json()),
initialPageParam: undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor,
getPreviousPageParam: (firstPage) => firstPage.prevCursor,
select: (data) => ({
pages: data.pages,
pageParams: data.pageParams,
items: data.pages.flatMap(page => page.items),
}),
});
// Complex infinite query with filtering
const filteredInfiniteQuery = (category: string, sortBy: string) => infiniteQueryOptions({
queryKey: ['products', 'infinite', category, sortBy],
queryFn: ({ queryKey, pageParam = 1 }) => {
const [, , category, sortBy] = queryKey;
return fetch(
`/api/products?category=${category}&sort=${sortBy}&page=${pageParam}`
).then(res => res.json());
},
initialPageParam: 1,
getNextPageParam: (lastPage, allPages) => {
if (lastPage.products.length < lastPage.pageSize) return undefined;
return allPages.length + 1;
},
select: (data) => ({
...data,
totalProducts: data.pages.reduce((sum, page) => sum + page.products.length, 0),
categories: [...new Set(data.pages.flatMap(page =>
page.products.map(p => p.category)
))],
}),
staleTime: 1000 * 60 * 5,
maxPages: 10, // Limit to prevent excessive memory usage
});
// Infinite query with optimistic updates
const commentsInfiniteQuery = (postId: number) => infiniteQueryOptions({
queryKey: ['post', postId, 'comments'],
queryFn: ({ pageParam = 1 }) =>
fetch(`/api/posts/${postId}/comments?page=${pageParam}`)
.then(res => res.json()),
initialPageParam: 1,
getNextPageParam: (lastPage, allPages) =>
lastPage.hasMore ? allPages.length + 1 : undefined,
select: (data) => ({
...data,
allComments: data.pages.flatMap(page => page.comments),
totalCount: data.pages[0]?.totalCount || 0,
}),
});
// Reusable infinite query factory
const createInfiniteListQuery = <T>(
endpoint: string,
pageSize: number = 20
) => infiniteQueryOptions({
queryKey: [endpoint, 'infinite'],
queryFn: ({ pageParam = 0 }) =>
fetch(`/api/${endpoint}?offset=${pageParam}&limit=${pageSize}`)
.then(res => res.json()) as Promise<{
items: T[];
hasMore: boolean;
total: number;
}>,
initialPageParam: 0,
getNextPageParam: (lastPage, allPages) =>
lastPage.hasMore ? allPages.length * pageSize : undefined,
staleTime: 1000 * 60 * 5,
});
// Use reusable factory
const usersInfiniteQuery = createInfiniteListQuery<User>('users');
const ordersInfiniteQuery = createInfiniteListQuery<Order>('orders', 50);Usage Examples:
// Query options with derived data
const createDerivedQuery = <TData, TDerived>(
baseOptions: Parameters<typeof queryOptions>[0],
derive: (data: TData) => TDerived
) => queryOptions({
...baseOptions,
select: (data: TData) => derive(data),
});
const statsQuery = createDerivedQuery(
{
queryKey: ['raw-stats'],
queryFn: () => fetch('/api/stats').then(res => res.json()),
},
(rawStats) => ({
summary: {
total: rawStats.reduce((sum, stat) => sum + stat.value, 0),
average: rawStats.length > 0 ? rawStats.reduce((sum, stat) => sum + stat.value, 0) / rawStats.length : 0,
max: Math.max(...rawStats.map(stat => stat.value)),
min: Math.min(...rawStats.map(stat => stat.value)),
},
byCategory: rawStats.reduce((acc, stat) => {
acc[stat.category] = (acc[stat.category] || 0) + stat.value;
return acc;
}, {}),
})
);
// Query options with error boundary
const createSafeQuery = <T>(
options: Parameters<typeof queryOptions>[0],
fallbackData: T
) => queryOptions({
...options,
select: (data) => data || fallbackData,
retry: (failureCount, error) => {
if (error.status === 404) return false;
return failureCount < 3;
},
throwOnError: false,
});
const safeUserQuery = (userId: number) => createSafeQuery(
{
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json()),
},
{ id: userId, name: 'Unknown User', email: '' }
);
// Prefetch helper using query options
const usePrefetch = () => {
const queryClient = useQueryClient();
const prefetchQuery = async (options: ReturnType<typeof queryOptions>) => {
await queryClient.prefetchQuery(options);
};
const prefetchInfiniteQuery = async (
options: ReturnType<typeof infiniteQueryOptions>
) => {
await queryClient.prefetchInfiniteQuery(options);
};
return { prefetchQuery, prefetchInfiniteQuery };
};
// Usage with prefetch
const { prefetchQuery } = usePrefetch();
const handleUserHover = (userId: number) => {
prefetchQuery(userQuery(userId));
};// Type enhancement for query options
type DataTag<TQueryKey, TQueryFnData, TError> = TQueryKey & {
_dataType?: TQueryFnData;
_errorType?: TError;
};
// Query options variants
interface DefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey>
extends UseQueryOptions<TQueryFnData, TError, TData, TQueryKey> {
initialData: TQueryFnData | (() => TQueryFnData);
}
interface UndefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey>
extends UseQueryOptions<TQueryFnData, TError, TData, TQueryKey> {
initialData?: undefined | (() => undefined);
}
// Infinite query options variants
interface DefinedInitialDataInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>
extends UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> {
initialData:
| InfiniteData<TQueryFnData, TPageParam>
| (() => InfiniteData<TQueryFnData, TPageParam>);
}
interface UndefinedInitialDataInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>
extends UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> {
initialData?: undefined;
}
// Data structure for infinite queries
interface InfiniteData<TData, TPageParam = unknown> {
pages: TData[];
pageParams: TPageParam[];
}
// Base query key type
type QueryKey = ReadonlyArray<unknown>;
// Error type
type DefaultError = Error;Install with Tessl CLI
npx tessl i tessl/npm-tanstack--vue-query