#Pass a generic infiniteQueryOptions object as parameter

4 messages · Page 1 of 1 (latest)

cerulean sky
#

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

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...

#
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,
  };
}
#

then when i go to use it like so

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,
    }),
  });