Below, I am sharing the code that I have written so far for row selection:

import {
  ApolloError,
  LazyQueryExecFunction,
  OperationVariables,
} from '@apollo/client';
import { CategoryList, Category } from '../common/lib/generated-types';
import {
  ColumnDef,
  flexRender,
  getCoreRowModel,
  useReactTable,
} from '@tanstack/react-table';
import classNames from 'classnames';
import { HTMLProps, useEffect, useRef, useState } from 'react';

export interface CategoriesProps {
  loading: boolean;
  error?: ApolloError;
  categories?: CategoryList;
  fetchCategories: LazyQueryExecFunction<
    {
      categories: CategoryList;
    },
    OperationVariables
  >;
  storeId: string;
}

const defaultColumns: ColumnDef<Category>[] = [
  {
    id: 'select',
    header: ({ table }) => (
      <IndeterminateCheckbox
        {...{
          checked: table.getIsAllRowsSelected(),
          indeterminate: table.getIsSomeRowsSelected(),
          onChange: table.getToggleAllRowsSelectedHandler(),
        }}
      />
    ),
    cell: ({ row }) => (
      <IndeterminateCheckbox
        {...{
          checked: row.getIsSelected(),
          indeterminate: row.getIsSomeSelected(),
          onChange: row.getToggleSelectedHandler(),
        }}
      />
    ),
  },
  {
    accessorKey: 'name',
    cell: info => info.getValue(),
    header: 'Category Name',
  },
  {
    accessorKey: 'slug',
    cell: info => info.getValue(),
    header: 'Slug',
  },
  {
    accessorKey: 'assignmentMethod',
    cell: info => info.getValue(),
    header: 'Assignment',
  },
  {
    accessorKey: 'isPrivate',
    cell: info => (info.getValue() ? 'Private' : 'Public'),
    header: 'Visibility',
  },
];

function IndeterminateCheckbox({
  indeterminate,
  className = '',
  ...rest
}: { indeterminate?: boolean } & HTMLProps<HTMLInputElement>) {
  const ref = useRef<HTMLInputElement>(null);

  useEffect(() => {
    if (typeof indeterminate === 'boolean' && ref.current) {
      ref.current.indeterminate = !rest.checked && indeterminate;
    }
  }, [ref, indeterminate, rest.checked]);

  return (
    <input
      type="checkbox"
      ref={ref}
      className={className + ' cursor-pointer'}
      {...rest}
    />
  );
}

export function Categories(props: CategoriesProps) {
  const { error, loading, categories, fetchCategories, storeId } = props;
  const [pagination, setPagination] = useState({
    pageIndex: 0,
    pageSize: 10,
  });
  const [sort, setSort] = useState({ name: 'ASC' });
  const [rowSelection, setRowSelection] = useState({});
  const pageCount = Math.ceil(
    (categories?.totalItems ?? 0) / pagination.pageSize,
  );

  const categoriesTable = useReactTable({
    data: categories?.items || [],
    columns: defaultColumns,
    columnResizeMode: 'onChange',
    pageCount,
    onRowSelectionChange: setRowSelection,
    getRowId: row => row.id.toString(),
    getCoreRowModel: getCoreRowModel(),
    state: {
      pagination,
      rowSelection,
    },
    onPaginationChange: setPagination,
    manualPagination: true,
  });

  useEffect(() => {
    fetchCategories({
      variables: {
        options: {
          take: pagination.pageSize,
          skip: pagination.pageIndex * pagination.pageSize,
          filter: { storeId: { eq: storeId } },
          sort,
        },
      },
    });
  }, [
    fetchCategories,
    pagination.pageIndex,
    pagination.pageSize,
    sort,
    storeId,
  ]);

  if (error) {
    return (
      <div>
        <h1>Error!</h1>
      </div>
    );
  }

  return (
    <div>
      <div className="overflow-x-auto">
        <table
          {...{
            style: {
              width: categoriesTable.getCenterTotalSize(),
            },
          }}
        >
          <thead>
            {categoriesTable.getHeaderGroups().map(headerGroup => (
              <tr key={headerGroup.id}>
                {headerGroup.headers.map(header => (
                  <th
                    {...{
                      key: header.id,
                      colSpan: header.colSpan,
                      style: {
                        textAlign: 'left',
                      },
                    }}
                    className="group relative"
                  >
                    {header.isPlaceholder
                      ? null
                      : flexRender(
                          header.column.columnDef.header,
                          header.getContext(),
                        )}
                    <div
                      {...{
                        onMouseDown: header.getResizeHandler(),
                        onTouchStart: header.getResizeHandler(),
                        className: classNames(
                          'absolute right-0 top-0 w-[5px] h-full bg-gray-200 cursor-col-resize user-select-none touch-action-none opacity-0 transition-opacity duration-200 group-hover:opacity-100',
                          {
                            'bg-blue-400 opacity-100':
                              header.column.getIsResizing(),
                          },
                        ),
                      }}
                    />
                  </th>
                ))}
              </tr>
            ))}
          </thead>

          {loading ? (
            <tbody>
              <tr>
                <td colSpan={4}>Loading...</td>
              </tr>
            </tbody>
          ) : (
            <tbody>
              {categoriesTable.getRowModel().rows.map(row => (
                <tr key={row.id}>
                  {row.getVisibleCells().map(cell => (
                    <td
                      {...{
                        key: cell.id,
                      }}
                    >
                      {flexRender(
                        cell.column.columnDef.cell,
                        cell.getContext(),
                      )}
                    </td>
                  ))}
                </tr>
              ))}
            </tbody>
          )}
        </table>
      </div>

      <div className="flex items-center gap-2">
        <button
          className="border rounded p-1"
          onClick={() => categoriesTable.setPageIndex(0)}
          disabled={!categoriesTable.getCanPreviousPage()}
        >
          {'<<'}
        </button>
        <button
          className="border rounded p-1"
          onClick={() => categoriesTable.previousPage()}
          disabled={!categoriesTable.getCanPreviousPage()}
        >
          {'<'}
        </button>
        <button
          className="border rounded p-1"
          onClick={() => categoriesTable.nextPage()}
          disabled={!categoriesTable.getCanNextPage()}
        >
          {'>'}
        </button>
        <button
          className="border rounded p-1"
          onClick={() =>
            categoriesTable.setPageIndex(categoriesTable.getPageCount() - 1)
          }
          disabled={!categoriesTable.getCanNextPage()}
        >
          {'>>'}
        </button>
        <span className="flex items-center gap-1">
          <div>Page</div>
          <strong>
            {categoriesTable.getState().pagination.pageIndex + 1} of{' '}
            {categoriesTable.getPageCount()}
          </strong>
        </span>
        <span className="flex items-center gap-1">
          | Go to page:
          <input
            type="number"
            value={categoriesTable.getState().pagination.pageIndex + 1}
            onChange={e => {
              const page = e.target.value ? Number(e.target.value) - 1 : 0;
              categoriesTable.setPageIndex(page);
            }}
            className="border p-1 rounded w-16"
          />
        </span>
        <select
          value={categoriesTable.getState().pagination.pageSize}
          onChange={e => {
            categoriesTable.setPageSize(Number(e.target.value));
          }}
        >
          {[10, 20, 30, 40, 50].map(pageSize => (
            <option key={pageSize} value={pageSize}>
              Show {pageSize}
            </option>
          ))}
        </select>
      </div>

      <div>{Object.keys(rowSelection).length} Rows Selected</div>
    </div>
  );
}

export default Categories;

What does the code do?

It fetches the paginated data from the server related to some categories - (Cat1 dataset, Cat2 dataset, Cat3 dataset)

Issue?

Row selection works well with data that resides locally.

But, let's say the paginated data is being fetched from the server, the limit is set to 10 per page and you have selected 7 rows from the 1st page. Now, go to page 2 and select 3 rows from the second page once the data is fetched. The Select All checkbox is unticked on page two whereas it should show indeterminate.

I believe this happens because row selection is only aware of 10 items per page. Although it has a memory of selected rows, it's not aware of out of how many items those rows were selected.

One of the approaches could be to save retrieved data in a state and pass it to useReactTable. But, the apollo client already handles caching for me. So, I do not have to save anything to state as clicking on next/previous provides a similar experience. Are there any other approaches where I can continue using apollo client's caching feature and still get the correct status of the Select All Checkbox?

I have created a code sandbox to reproduce the issue:

https://codesandbox.io/s/pedantic-austin-uqt9ry?file=/src/table.tsx

0

There are 0 best solutions below