Live Pagination

Live pagination

InfiniteTable supports live pagination in its DataSource via the livePagination prop together with livePaginationCursor

Specify DataSource.livePagination=true and provide a pagination cursor (a good cursor would be the id of the last item in the DataSource).

In addition, you have to listen to onDataParamsChange which will be triggered with an object that contains the following properties:

  • sortInfo - information about the current sort state
  • groupBy - current grouping info
  • livePaginationCursor - the current pagination cursor

When dataParams change (you will be notified via onDataParamsChange), you have to fetch new data using the cursor from dataParams object.

onDataParamsChange trigger

Basically onDataParamsChange is triggered whenever props (and state) that affect the DataSource change - be it via sorting, filtering, live pagination, pivoting, etc.

Below you can see a live pagination demo implemented in combination with react-query.

Live pagination - with react-query
View Mode
Fork
import {
  InfiniteTable,
  InfiniteTableColumn,
  DataSource,
  DataSourceSingleSortInfo,
  DataSourceDataParams,
  DataSourceLivePaginationCursorFn,
} from '@infinite-table/infinite-react';
import * as React from 'react';
import { useCallback } from 'react';
import {
  QueryClient,
  QueryClientProvider,
  useInfiniteQuery,
} from 'react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: false,
    },
  },
});

const emptyArray: Employee[] = [];

export const columns: Record<string, InfiniteTableColumn<Employee>> = {
  id: { field: 'id' },
  country: {
    field: 'country',
  },
  city: { field: 'city' },
  team: { field: 'team' },
  department: { field: 'department' },
  firstName: { field: 'firstName' },
  lastName: { field: 'lastName' },

  salary: { field: 'salary' },
  age: { field: 'age' },
};

type Employee = {
  id: number;
  companyName: string;
  companySize: string;
  firstName: string;
  lastName: string;
  country: string;
  countryCode: string;
  city: string;
  streetName: string;
  streetNo: number;
  department: string;
  team: string;
  salary: number;
  age: number;
  email: string;
};

const PAGE_SIZE = 10;

const dataSource = ({
  sortInfo,
  livePaginationCursor = 0,
}: {
  sortInfo: DataSourceSingleSortInfo<Employee> | null;
  livePaginationCursor: number;
}) => {
  return fetch(
    'https://infinite-table.com/.netlify/functions/json-server' +
      `/employees10k?_limit=${PAGE_SIZE}&_sort=${sortInfo?.field}&_order=${
        sortInfo?.dir === 1 ? 'asc' : 'desc'
      }&_start=${livePaginationCursor}`,
  )
    .then(async (r) => {
      const data = await r.json();
      // we need the remote count, so we take it from headers
      const total = Number(r.headers.get('X-Total-Count')!);
      return { data, total };
    })
    .then(({ data, total }: { data: Employee[]; total: number }) => {
      const page = livePaginationCursor / PAGE_SIZE + 1;

      const prevPageCursor = Math.max(PAGE_SIZE * (page - 1), 0);
      return {
        data,
        hasMore: total > PAGE_SIZE * page,
        page,
        prevPageCursor,
        nextPageCursor: prevPageCursor + data.length,
      };
    })
    .then(
      (
        response,
      ): Promise<{
        data: Employee[];
        hasMore: boolean;
        page: number;
        nextPageCursor: number;
        prevPageCursor: number;
      }> => {
        return new Promise((resolve) => {
          setTimeout(() => {
            resolve(response);
          }, 150);
        });
      },
    );
};

const Example = () => {
  const [dataParams, setDataParams] = React.useState<
    Partial<DataSourceDataParams<Employee>>
  >({
    groupBy: [],
    sortInfo: undefined,
    livePaginationCursor: null,
  });
  const {
    data,
    fetchNextPage: fetchNext,
    isFetchingNextPage,
  } = useInfiniteQuery(
    ['employees', dataParams.sortInfo, dataParams.groupBy],
    ({ pageParam = 0 }) => {
      const params = {
        livePaginationCursor: pageParam,
        sortInfo:
          dataParams.sortInfo as DataSourceSingleSortInfo<Employee> | null,
      };

      return dataSource(params);
    },

    {
      keepPreviousData: true,
      getPreviousPageParam: (firstPage) => firstPage.prevPageCursor || 0,
      getNextPageParam: (lastPage) => {
        const nextPageCursor = lastPage.hasMore
          ? lastPage.nextPageCursor
          : undefined;

        return nextPageCursor;
      },

      select: (data) => {
        const flatData = data.pages.flatMap((x) => x.data);
        const nextPageCursor = data.pages[data.pages.length - 1].nextPageCursor;

        const result = {
          pages: flatData,
          pageParams: [nextPageCursor],
        };

        return result;
      },
    },
  );

  const onDataParamsChange = useCallback(
    (dataParams: DataSourceDataParams<Employee>) => {
      const params = {
        groupBy: dataParams.groupBy,
        sortInfo: dataParams.sortInfo,
        livePaginationCursor: dataParams.livePaginationCursor,
      };

      setDataParams(params);
    },
    [],
  );

  const [scrollTopId, setScrollTop] = React.useState(0);

  React.useEffect(() => {
    // when sorting changes, scroll to the top
    setScrollTop(Date.now());
  }, [dataParams.sortInfo]);

  const fetchNextPage = () => {
    if (isFetchingNextPage) {
      return;
    }

    fetchNext();
  };

  React.useEffect(() => {
    fetchNextPage();
  }, [dataParams.livePaginationCursor]);

  const livePaginationCursorFn: DataSourceLivePaginationCursorFn<Employee> =
    useCallback(({ length }) => {
      return length;
    }, []);

  return (
    <React.StrictMode>
      <DataSource<Employee>
        primaryKey="id"
        // take the data from `data.pages`,
        // as returned from our react-query select function

        sortInfo={dataParams?.sortInfo}
        data={data?.pages || emptyArray}
        loading={isFetchingNextPage}
        onDataParamsChange={onDataParamsChange}
        livePagination
        livePaginationCursor={livePaginationCursorFn}
      >
        <InfiniteTable<Employee>
          scrollTopKey={scrollTopId}
          columnDefaultWidth={200}
          columns={columns}
        />
      </DataSource>
    </React.StrictMode>
  );
};

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Example />
    </QueryClientProvider>

In the example above, play around and scroll the table and also make sure to try sorting (eg: sort by country or city).

Note

For demo purposes, the page size in the example above is small - it shows that InfiniteTable handles infinite pagination correctly by immediately triggering onDataParamsChange when there are not enough rows to fill the viewport.

On the other hand, when there are many rows and there is a horizontal scrollbar, it triggers onDataParamsChange only when the user scrolls to the end of the table.

It also handles the case when there is a vertical scrollbar and then the user resizes the viewport to make it bigger and no more vertical scrollbar is needed - again onDataParamsChange is triggered to request more rows.