Pass a generic infiniteQueryOptions object as parameter
I posted in the main thread but adding a question here... I feel like I'm very close to getting the types working but could just be something that isn't supported
I am struggling with type issues a bit - i am trying to create a generic hook that can take in a infiniteQueryOptions that will ensure a "standard" PaginatedResponse object from the queryFn. I dont have any errors in types on my useGeneric function but when i go to actually use it i run into a type errors with queryKeys being readonly. Is this something that just isn't possible? (https://github.com/TanStack/query/issues/7974) i found this issue but is bit different use case than mine
GitHub
Type Error when using
useQueries
with results returning different...Describe the bug I want to use useQueries to fetch x queries every time, and another y queries, whose count is not known at compile time. Here is a short example (playground below): export const us...
1 Reply
plain-purpleOP•6d ago
export type InfiniteQueryOptionsFn<
TQueryFnData,
TError = DefaultError,
TData = InfiniteData<TQueryFnData>,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
> = (params: {
search?: string;
pageSize?: number;
}) => DefinedInitialDataInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>;
export type FetchMissingQueryOptionsFn<TItem> = (params: {
ids: string[];
}) => ReturnType<typeof queryOptions<{ items: TItem[] }, Error>>;
interface UseGenericSearchOptionsProps<TItem, TPage, TQueryKey extends QueryKey = QueryKey> {
infiniteQueryOptions: InfiniteQueryOptionsFn<TPage, DefaultError, InfiniteData<TPage>, TQueryKey, number>;
idsQueryOptionsFn: FetchMissingQueryOptionsFn<TItem>;
mapToOptions: (item: TItem) => ColumnOption;
initialSelected?: TItem[] | TItem;
initialSelectedIds?: string[];
pageSize?: number;
}
export type InfiniteQueryOptionsFn<
TQueryFnData,
TError = DefaultError,
TData = InfiniteData<TQueryFnData>,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
> = (params: {
search?: string;
pageSize?: number;
}) => DefinedInitialDataInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>;
export type FetchMissingQueryOptionsFn<TItem> = (params: {
ids: string[];
}) => ReturnType<typeof queryOptions<{ items: TItem[] }, Error>>;
interface UseGenericSearchOptionsProps<TItem, TPage, TQueryKey extends QueryKey = QueryKey> {
infiniteQueryOptions: InfiniteQueryOptionsFn<TPage, DefaultError, InfiniteData<TPage>, TQueryKey, number>;
idsQueryOptionsFn: FetchMissingQueryOptionsFn<TItem>;
mapToOptions: (item: TItem) => ColumnOption;
initialSelected?: TItem[] | TItem;
initialSelectedIds?: string[];
pageSize?: number;
}
export function useGenericSearchOptions<TItem, TPage extends PaginatedData<TItem>>({
infiniteQueryOptions,
idsQueryOptionsFn,
mapToOptions,
initialSelected,
initialSelectedIds = [],
pageSize = 10,
}: UseGenericSearchOptionsProps<TItem, TPage>) {
const [searchValue, setSearchValue] = useState('');
const [selectedItems, setSelectedItems] = useState<TItem[]>(
Array.isArray(initialSelected) ? initialSelected : initialSelected ? [initialSelected] : [],
);
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery(
infiniteQueryOptions({ search: searchValue, pageSize }),
);
// Flatten pages into items
const items: TItem[] = useMemo(() => data?.pages.flatMap((page) => page.data) ?? [], [data]);
const fetchedOptions = useMemo(() => items.map(mapToOptions), [items, mapToOptions]);
// Build a lookup map for deduplication
const optionsMap = useMemo(() => new Map(fetchedOptions.map((opt) => [opt.value, opt])), [fetchedOptions]);
// Fetch missing preselected IDs
const missingIds = initialSelectedIds.filter((id) => !optionsMap.has(id));
const missingSelected = useQuery(idsQueryOptionsFn({ ids: missingIds }));
const missingSelectedOptions = useMemo(
() => (missingSelected.data?.items ?? []).map(mapToOptions),
[missingSelected.data, mapToOptions],
);
// Merge: fetched + selected + missing
const mergedOptions = useMemo(() => {
const selectedOptions = selectedItems.map(mapToOptions);
const missingOptions = selectedOptions.filter((opt) => !optionsMap.has(opt.value));
return [...fetchedOptions, ...missingOptions, ...missingSelectedOptions];
}, [fetchedOptions, selectedItems, missingSelectedOptions, optionsMap, mapToOptions]);
return {
options: mergedOptions,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
searchValue,
setSearchValue,
selectedItems,
setSelectedItems,
};
}
export function useGenericSearchOptions<TItem, TPage extends PaginatedData<TItem>>({
infiniteQueryOptions,
idsQueryOptionsFn,
mapToOptions,
initialSelected,
initialSelectedIds = [],
pageSize = 10,
}: UseGenericSearchOptionsProps<TItem, TPage>) {
const [searchValue, setSearchValue] = useState('');
const [selectedItems, setSelectedItems] = useState<TItem[]>(
Array.isArray(initialSelected) ? initialSelected : initialSelected ? [initialSelected] : [],
);
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery(
infiniteQueryOptions({ search: searchValue, pageSize }),
);
// Flatten pages into items
const items: TItem[] = useMemo(() => data?.pages.flatMap((page) => page.data) ?? [], [data]);
const fetchedOptions = useMemo(() => items.map(mapToOptions), [items, mapToOptions]);
// Build a lookup map for deduplication
const optionsMap = useMemo(() => new Map(fetchedOptions.map((opt) => [opt.value, opt])), [fetchedOptions]);
// Fetch missing preselected IDs
const missingIds = initialSelectedIds.filter((id) => !optionsMap.has(id));
const missingSelected = useQuery(idsQueryOptionsFn({ ids: missingIds }));
const missingSelectedOptions = useMemo(
() => (missingSelected.data?.items ?? []).map(mapToOptions),
[missingSelected.data, mapToOptions],
);
// Merge: fetched + selected + missing
const mergedOptions = useMemo(() => {
const selectedOptions = selectedItems.map(mapToOptions);
const missingOptions = selectedOptions.filter((opt) => !optionsMap.has(opt.value));
return [...fetchedOptions, ...missingOptions, ...missingSelectedOptions];
}, [fetchedOptions, selectedItems, missingSelectedOptions, optionsMap, mapToOptions]);
return {
options: mergedOptions,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
searchValue,
setSearchValue,
selectedItems,
setSelectedItems,
};
}
const getProjectsInfiniteQuery = ({ search, pageSize = 10 }: { search?: string; pageSize?: number }) => {
const dto: GetProjectCoresDTOBase = {
page: 1,
pageSize,
globalSearch: search,
sorting: [{ id: ProjectTableKeys.name, desc: false }],
};
return infiniteQueryOptions({
queryKey: projectKeys.core.all(dto),
queryFn: async ({ pageParam = 1 }) => {
return ProjectService.list({
page: pageParam,
pageSize: 10,
globalSearch: search,
sorting: [{ id: ProjectTableKeys.name, desc: false }],
});
},
getNextPageParam: (lastPage, allPages) => {
const lastPageData = lastPage as GetProjectCoresResponseDTO;
const nextPage = lastPageData.page + 1;
return nextPage <= lastPageData.totalPages ? nextPage : undefined;
},
initialPageParam: 1,
});
};
useGenericSearchOptions({
infiniteQueryOptions: getProjectsInfiniteQuery,
idsQueryOptionsFn: getMissingProjectCoresQuery,
mapToOptions: (item) => ({
value: item.id,
label: item.name,
}),
});
const getProjectsInfiniteQuery = ({ search, pageSize = 10 }: { search?: string; pageSize?: number }) => {
const dto: GetProjectCoresDTOBase = {
page: 1,
pageSize,
globalSearch: search,
sorting: [{ id: ProjectTableKeys.name, desc: false }],
};
return infiniteQueryOptions({
queryKey: projectKeys.core.all(dto),
queryFn: async ({ pageParam = 1 }) => {
return ProjectService.list({
page: pageParam,
pageSize: 10,
globalSearch: search,
sorting: [{ id: ProjectTableKeys.name, desc: false }],
});
},
getNextPageParam: (lastPage, allPages) => {
const lastPageData = lastPage as GetProjectCoresResponseDTO;
const nextPage = lastPageData.page + 1;
return nextPage <= lastPageData.totalPages ? nextPage : undefined;
},
initialPageParam: 1,
});
};
useGenericSearchOptions({
infiniteQueryOptions: getProjectsInfiniteQuery,
idsQueryOptionsFn: getMissingProjectCoresQuery,
mapToOptions: (item) => ({
value: item.id,
label: item.name,
}),
});
Type 'OmitKeyof<UseInfiniteQueryOptions<GetProjectCoresResponseDTO, Error, InfiniteData<GetProjectCoresResponseDTO, unknown>, GetProjectCoresResponseDTO, (string | GetProjectCoresDTOBase)[], number>, "queryFn"> & { ...; } & { ...; }' is not assignable to type 'UseInfiniteQueryOptions<GetProjectCoresResponseDTO, Error, InfiniteData<GetProjectCoresResponseDTO, unknown>, GetProjectCoresResponseDTO, readonly unknown[], number>'.
Types of property 'queryFn' are incompatible.
Type 'QueryFunction<GetProjectCoresResponseDTO, (string | GetProjectCoresDTOBase)[], number> | undefined' is not assignable to type 'unique symbol | QueryFunction<GetProjectCoresResponseDTO, readonly unknown[], number> | undefined'.
Type 'QueryFunction<GetProjectCoresResponseDTO, (string | GetProjectCoresDTOBase)[], number>' is not assignable to type 'unique symbol | QueryFunction<GetProjectCoresResponseDTO, readonly unknown[], number> | undefined'.
Type 'QueryFunction<GetProjectCoresResponseDTO, (string | GetProjectCoresDTOBase)[], number>' is not assignable to type 'QueryFunction<GetProjectCoresResponseDTO, readonly unknown[], number>'.
The type 'readonly unknown[]' is 'readonly' and cannot be assigned to the mutable type '(string | GetProjectCoresDTOBase)[]'.ts(2322)
useProjects.tsx(458, 3): The expected type comes from property 'infiniteQueryOptions' which is declared here on type 'UseGenericSearchOptionsProps<ProjectCore, GetProjectCoresResponseDTO, readonly unknown[]>'
Type 'OmitKeyof<UseInfiniteQueryOptions<GetProjectCoresResponseDTO, Error, InfiniteData<GetProjectCoresResponseDTO, unknown>, GetProjectCoresResponseDTO, (string | GetProjectCoresDTOBase)[], number>, "queryFn"> & { ...; } & { ...; }' is not assignable to type 'UseInfiniteQueryOptions<GetProjectCoresResponseDTO, Error, InfiniteData<GetProjectCoresResponseDTO, unknown>, GetProjectCoresResponseDTO, readonly unknown[], number>'.
Types of property 'queryFn' are incompatible.
Type 'QueryFunction<GetProjectCoresResponseDTO, (string | GetProjectCoresDTOBase)[], number> | undefined' is not assignable to type 'unique symbol | QueryFunction<GetProjectCoresResponseDTO, readonly unknown[], number> | undefined'.
Type 'QueryFunction<GetProjectCoresResponseDTO, (string | GetProjectCoresDTOBase)[], number>' is not assignable to type 'unique symbol | QueryFunction<GetProjectCoresResponseDTO, readonly unknown[], number> | undefined'.
Type 'QueryFunction<GetProjectCoresResponseDTO, (string | GetProjectCoresDTOBase)[], number>' is not assignable to type 'QueryFunction<GetProjectCoresResponseDTO, readonly unknown[], number>'.
The type 'readonly unknown[]' is 'readonly' and cannot be assigned to the mutable type '(string | GetProjectCoresDTOBase)[]'.ts(2322)
useProjects.tsx(458, 3): The expected type comes from property 'infiniteQueryOptions' which is declared here on type 'UseGenericSearchOptionsProps<ProjectCore, GetProjectCoresResponseDTO, readonly unknown[]>'