Skip to content

Using RTK Infinite Query For Tabular Pagination #4936

@elberttimothy

Description

@elberttimothy

Hi Redux maintainers,

Very grateful for the new RTK Infinite Query API. I think this solves a lot of the pain points with manually merging paginated requests and cursor management. However, given that it's mainly built for infinite scrolling UIs, we've had to implement our own logic to make this API usable for tabular pagination.

The code snippet below highlights how we're trying to go about this. Almost every table in our app are currently server-side paginated and so we're trying to opt for a higher-order hook (hook factory) pattern to wrap the infinite query hook with all the extra components that our tables need.

However, there are some pain points when working with the types in the current version of the package. Namely, TypedUseInfiniteQuery under generic conditions only returns:

  • refetch
  • fetchNextPage
  • fetchPreviousPage

I don't understand why data for instance, could not just be returned as InfiniteData<ResultType, PageParam> or why the booleans like isFetching and hasNextPage are not there.

Any advice on how to go about this? Perhaps this is the wrong pattern to use?

import {
  type BaseQueryFn,
  type InfiniteData,
  type TypedUseInfiniteQuery,
} from '@reduxjs/toolkit/query/react';
import { useMemo, useRef, useState } from 'react';

type PaginationState = {
  pageIndex: number;
  pageSize: number;
};

export type CreatePaginatedQueryHookPageParam = {
  limit?: number;
};

export const createPaginatedInfiniteQueryHook = <
  ResultType,
  QueryArg,
  PageParam extends CreatePaginatedQueryHookPageParam,
  BaseQuery extends BaseQueryFn,
>(
  useInfiniteQuery: TypedUseInfiniteQuery<
    ResultType,
    QueryArg,
    PageParam,
    BaseQuery
  >,
) => {
  type UseInfiniteQueryParameters = Parameters<
    TypedUseInfiniteQuery<ResultType, QueryArg, PageParam, BaseQuery>
  >;
  type UsePaginatedQueryArgs = UseInfiniteQueryParameters[0];
  type UsePaginatedQuerySubscriptionOptions = Extract<
    UseInfiniteQueryParameters[1],
    object
  >;
  
  // this is the returned pagination hook
  return (
    queryArgs: UsePaginatedQueryArgs,
    subscriptionOptions: UsePaginatedQuerySubscriptionOptions,
  ) => {
    const { initialPageParam } = subscriptionOptions;
    if (!initialPageParam?.limit) {
      throw new Error(
        'Paginated query hook requires initialPageParam with limit',
      );
    }

    // client-side pagination state for components to display
    const furthestPageIndex = useRef(0);
    const [{ pageIndex, pageSize }, setPaginationState] =
      useState<PaginationState>({
        pageIndex: 0,
        pageSize: initialPageParam.limit,
      });
    const { refetch, fetchNextPage, ...result } = useInfiniteQuery(
      queryArgs,
      subscriptionOptions,
    );

    /** Go forwards by 1 page. */
    const getNextPage = async () => {
      if (!result.hasNextPage) return;
      if (pageIndex + 1 > furthestPageIndex.current) {
        await fetchNextPage();
        furthestPageIndex.current += 1;
      }
      setPaginationState(({ pageSize, pageIndex }) => ({
        pageSize,
        pageIndex: pageIndex + 1,
      }));
    };

    /**
     * Go back by 1 page.
     */
    const getPrevPage = async () => {
      if (pageIndex <= 0) return;
      setPaginationState(({ pageSize, pageIndex }) => ({
        pageSize,
        pageIndex: pageIndex - 1,
      }));
    };

    /**
     * Changing the limit will always reset pagination to `pageIndex` 0.
     */
    const changeLimitAndResetPagination = async (newLimit: number) => {
      furthestPageIndex.current = 0;
      setPaginationState({
        pageIndex: 0,
        pageSize: newLimit,
      });
      await refetch();
    };

    /**
     * Consumers should call this when `QueryArgs/PageParams` for `useInfiniteQuery` changes.
     */
    const refetchAndResetPagination = async () => {
      furthestPageIndex.current = 0;
      setPaginationState(({ pageSize }) => ({
        pageIndex: 0,
        pageSize: pageSize,
      }));
      await refetch();
    };
    
    // we have to typecast data here
    const data =
      'data' in result
        ? (result.data as InfiniteData<ResultType, PageParam>)
        : undefined;

    const currentPage = useMemo(() => {
      if (data) {
        return data.pages[pageIndex];
      }
    }, [pageIndex, data]);

    return {
      getNextPage,
      getPrevPage,
      changeLimitAndResetPagination,
      refetchAndResetPagination,
      currentPage,
      paginationState: {
        pageIndex,
        pageSize,
        canGetNextPage: result.hasNextPage as boolean,
        canGetPrevPage: pageIndex > 0,
      },
      infiniteQueryResults: result,
    };
  };
};

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions