reactjstypescriptreact-table-v8

unable to sort react table


here's the entire component

import {
  ColumnDef,
  flexRender,
  SortingState,
  useReactTable,
  getCoreRowModel,
} from "@tanstack/react-table";
import { useIntersectionObserver } from "@/hooks";
import {
  Box,
  Flex,
  Text,
  Paper,
  Table,
  Skeleton,
  BoxProps,
  useMantineTheme,
} from "@mantine/core";
import {
  forwardRef,
  ReactNode,
  useEffect,
  useState,
  useMemo,
  useRef,
  memo,
} from "react";
import { QueryFunction, useInfiniteQuery } from "@tanstack/react-query";
import { RiDatabase2Line } from "react-icons/ri";
import { css } from "@emotion/css";

export type FetchFn<T> = (props: { pageParam: number }) => Promise<T[]>;

interface TableComponentProps<T> {
  queryFn: QueryFunction<T[], string[], number>;
  columns: ColumnDef<T, any>[];
  queryKey: any[];
}

const TableComponent: <T>(props: TableComponentProps<T>) => ReactNode = memo(
  ({ queryKey, queryFn, columns }) => {
    const skullComponentRef = useRef<HTMLDivElement>(null);
    const [sortingState, setSortingState] = useState<SortingState>([]);
    const entry = useIntersectionObserver(skullComponentRef, {});
    const isVisible = !!entry?.isIntersecting;
    const { colors } = useMantineTheme();

    const {
      data,
      isLoading,
      fetchStatus,
      hasNextPage,
      isRefetching,
      fetchNextPage,
    } = useInfiniteQuery({
      queryFn,
      initialPageParam: 0,
      refetchOnWindowFocus: false,
      staleTime: 1 * 60 * 1000, // 1 min
      queryKey: [...queryKey, sortingState],
      getNextPageParam: (lastPage, allpages) =>
        lastPage.length ? allpages.length : null,
    });

    const scrollPingPong = () => {
      const tableContainer = document.querySelector<HTMLDivElement>(
        ".mantine-ScrollArea-viewport"
      );
      if (tableContainer && skullComponentRef.current) {
        tableContainer.scrollTop =
          tableContainer.scrollHeight -
          tableContainer.clientHeight -
          skullComponentRef.current.offsetHeight;
        tableContainer.scrollTop =
          tableContainer.scrollHeight - tableContainer.clientHeight;
      }
    };

    const flatData = useMemo(
      () => data?.pages?.flatMap((row) => row) ?? [],
      [data]
    );

    const table = useReactTable({
      columns,
      data: flatData,
      state: { sorting: sortingState },
      onSortingChange: setSortingState,
      getCoreRowModel: getCoreRowModel(),
    });

    useEffect(() => {
      if (isVisible && hasNextPage) {
        scrollPingPong();
        fetchNextPage();
      }
    }, [isVisible, hasNextPage, fetchNextPage]);

    return (
      <Paper
        mt="md"
        bg="white"
        withBorder
        radius={"lg"}
        style={{ overflow: "hidden" }}
      >
        <Table.ScrollContainer
          minWidth={800}
          h={`calc(100vh - 80px - 4rem)`}
          className={css({
            ".mantine-ScrollArea-viewport": { paddingBottom: 0 },
          })}
        >
          <Table
            className={css({
              "tbody tr:nth-child(even)": {
                background: colors.tailwind_gray[0],
              },
            })}
            verticalSpacing="md"
            horizontalSpacing="xl"
            withRowBorders={false}
          >
            <Table.Thead
              h="4rem"
              c="white"
              bg={colors.tailwind_indigo[6]}
              style={{ position: "sticky", top: 0, zIndex: 1 }}
            >
              {table.getHeaderGroups().map((headerGroup) => (
                <Table.Tr key={headerGroup.id}>
                  {headerGroup.headers.map((header) => (
                    <Table.Th
                      style={{ cursor: "pointer" }}
                      key={header.id}
                      onClick={() => {
                        header.column.toggleSorting();
                      }}
                    >
                      {header.isPlaceholder
                        ? null
                        : flexRender(
                            header.column.columnDef.header,
                            header.getContext()
                          )}
                      {{
                        asc: " 🔼",
                        desc: " 🔽",
                      }[header.column.getIsSorted() as string] ?? null}
                    </Table.Th>
                  ))}
                </Table.Tr>
              ))}
            </Table.Thead>
            <Table.Tbody
              style={{
                maxHeight: "calc(100vh - 134px)",
                overflowY: "auto",
              }}
              className={css({
                "& tr:hover": {
                  transition: "background .1s ease",
                  background: `${colors.tailwind_gray[1]} !important`,
                },
              })}
            >
              {table.getRowModel().rows.map((row) => (
                <Table.Tr style={{ cursor: "pointer" }} key={row.id}>
                  {row.getVisibleCells().map((cell) => (
                    <Table.Td
                      key={cell.id}
                      onClick={() => {
                        cell.column.id !== "action" && row.toggleSelected();
                      }}
                    >
                      {flexRender(
                        cell.column.columnDef.cell,
                        cell.getContext()
                      )}
                    </Table.Td>
                  ))}
                </Table.Tr>
              ))}
            </Table.Tbody>
          </Table>

          {(hasNextPage || isRefetching || isLoading) && (
            <Skull
              skullAmount={isLoading ? 10 : 3}
              ref={skullComponentRef}
              px="xs"
            />
          )}
          {fetchStatus === "idle" && !hasNextPage && (
            <Flex
              py="lg"
              gap="xs"
              h="100%"
              align="center"
              tt="capitalize"
              justify="center"
              c="tailwind_submarine.9"
              bg="tailwind_submarine.0"
            >
              <RiDatabase2Line size={25} />
              <Text>no further records found.</Text>
            </Flex>
          )}
        </Table.ScrollContainer>
      </Paper>
    );
  }
);

const Skull = forwardRef<HTMLDivElement, BoxProps & { skullAmount: number }>(
  ({ skullAmount, ...props }, ref) => {
    return (
      <Box ref={ref} {...props}>
        {Array(skullAmount)
          .fill(null)
          .flatMap((_, key) => {
            return <Skeleton h="3.5rem" mt="xs" key={key} />;
          })}
      </Box>
    );
  }
);

export default TableComponent;

here it's being used

import {
  Menu,
  Modal,
  Flex,
  Title,
  Stack,
  Button,
  Checkbox,
  ActionIcon,
  useMantineTheme,
} from "@mantine/core";
import { useQueryClient, useMutation } from "@tanstack/react-query";
import { IRetailer } from "@/providers/onboardingformprovider";
import { RiDeleteBin6Line, RiEdit2Line } from "react-icons/ri";
import { createColumnHelper } from "@tanstack/react-table";
import { BsThreeDotsVertical } from "react-icons/bs";
import TableComponent, { FetchFn } from "./table";
import { useDisclosure } from "@mantine/hooks";
import { useState } from "react";

interface Product
  extends Pick<
    IRetailer,
    | "productname"
    | "priceperunit"
    | "productquantity"
    | "productcategory"
    | "productdescription"
  > {
  check?: any;
  action?: any;
}

const columnHelper = createColumnHelper<Product>();

const ProductsList = () => {
  const [selectedProduct, setSelectedProduct] = useState<Product>();
  const [opened, { open, close }] = useDisclosure(false);
  const [products, setProducts] = useState<Product[]>([
    {
      productname: "one",
      priceperunit: 10,
      productcategory: "test 1",
      productdescription: "test 1",
      productquantity: 10,
    },
    {
      productname: "two",
      priceperunit: 10,
      productcategory: "test 2",
      productdescription: "test 1",
      productquantity: 10,
    },
    {
      productname: "three",
      priceperunit: 10,
      productcategory: "test 3",
      productdescription: "test 1",
      productquantity: 10,
    },
    {
      productname: "four",
      priceperunit: 10,
      productcategory: "test 4",
      productdescription: "test 1",
      productquantity: 10,
    },
    {
      productname: "five",
      priceperunit: 10,
      productcategory: "test 5",
      productdescription: "test 1",
      productquantity: 10,
    },
    {
      productname: "six",
      priceperunit: 10,
      productcategory: "test 6",
      productdescription: "test 1",
      productquantity: 10,
    },
    {
      productname: "seven",
      priceperunit: 10,
      productcategory: "test 7",
      productdescription: "test 1",
      productquantity: 10,
    },
    {
      productname: "eight",
      priceperunit: 10,
      productcategory: "test 8",
      productdescription: "test 1",
      productquantity: 10,
    },
    {
      productname: "nine",
      priceperunit: 10,
      productcategory: "test 9",
      productdescription: "test 1",
      productquantity: 10,
    },
    {
      productname: "ten",
      priceperunit: 10,
      productcategory: "test 10",
      productdescription: "test 1",
      productquantity: 10,
    },
    {
      productname: "eleven",
      priceperunit: 10,
      productcategory: "test 1",
      productdescription: "test 1",
      productquantity: 10,
    },
    {
      productname: "twelve",
      priceperunit: 10,
      productcategory: "test 2",
      productdescription: "test 1",
      productquantity: 10,
    },
    {
      productname: "thirteen",
      priceperunit: 10,
      productcategory: "test 3",
      productdescription: "test 1",
      productquantity: 10,
    },
    {
      productname: "fourteen",
      priceperunit: 10,
      productcategory: "test 4",
      productdescription: "test 1",
      productquantity: 10,
    },
    {
      productname: "fifteen",
      priceperunit: 10,
      productcategory: "test 5",
      productdescription: "test 1",
      productquantity: 10,
    },
    {
      productname: "sixteen",
      priceperunit: 10,
      productcategory: "test 6",
      productdescription: "test 1",
      productquantity: 10,
    },
    {
      productname: "seventeen",
      priceperunit: 10,
      productcategory: "test 7",
      productdescription: "test 1",
      productquantity: 10,
    },
    {
      productname: "eighteen",
      priceperunit: 10,
      productcategory: "test 8",
      productdescription: "test 1",
      productquantity: 10,
    },
    {
      productname: "nineteen",
      priceperunit: 10,
      productcategory: "test 9",
      productdescription: "test 1",
      productquantity: 10,
    },
    {
      productname: "twenty",
      priceperunit: 10,
      productcategory: "test 10",
      productdescription: "test 1",
      productquantity: 10,
    },
  ]);
  const { colors } = useMantineTheme();
  const client = useQueryClient();

  const DeleteProductsMuation = useMutation({
    mutationFn: () =>
      Promise.resolve(
        setProducts((products) => {
          return products.filter(
            (product) => selectedProduct?.productname !== product.productname
          );
        })
      ),
    onSuccess: () => {
      client.resetQueries({
        queryKey: ["products"],
      });
    },
  });

  const columns = [
    columnHelper.accessor("check", {
      header: ({ table }) => (
        <Checkbox
          color="tailwind_indigo.4"
          checked={table.getIsAllPageRowsSelected()}
          indeterminate={table.getIsSomeRowsSelected()}
          onChange={() => {
            table.toggleAllPageRowsSelected();
          }}
        />
      ),
      cell: ({ row }) => <Checkbox checked={row.getIsSelected()} readOnly />,
    }),
    columnHelper.accessor("productname", {
      cell: (info) => info.getValue(),
      header: "Product Name",
    }),
    columnHelper.accessor("productcategory", {
      cell: (info) => info.getValue(),
      header: "Product Category",
      enableSorting: true,
    }),
    columnHelper.accessor("productquantity", {
      cell: (info) => info.getValue(),
      header: "Product Quantity",
    }),
    columnHelper.accessor("priceperunit", {
      cell: (info) => info.getValue(),
      header: "Price",
    }),
    columnHelper.accessor("action", {
      header: "Action",
      cell: ({ row }) => (
        <Menu
          shadow="xl"
          radius="md"
          width={220}
          position="bottom-end"
          styles={{
            dropdown: {
              border: "1px solid",
              borderColor: colors.tailwind_gray[0],
            },
            item: {
              padding: ".5rem",
            },
          }}
        >
          <Menu.Target>
            <ActionIcon
              color="tailwind_gray.2"
              variant="outline"
              size="2.5rem"
              radius="xl"
              c="black"
            >
              <BsThreeDotsVertical />
            </ActionIcon>
          </Menu.Target>
          <Menu.Dropdown>
            <Menu.Item
              leftSection={
                <RiEdit2Line
                  size={16}
                  color="var(--mantine-color-tailwind_gray-6)"
                />
              }
            >
              Edit Product
            </Menu.Item>
            <Menu.Item
              color="red"
              leftSection={<RiDeleteBin6Line size={15} />}
              onClick={() => {
                setSelectedProduct(row.original);
                open();
              }}
            >
              Delete Product
            </Menu.Item>
          </Menu.Dropdown>
        </Menu>
      ),
    }),
  ];

  const fetchProducts: FetchFn<Product> = async ({ pageParam }) => {
    return await new Promise((resolve) => {
      // !add offset and limit/length should be statically type like (20 or 7), offset should be the pageParam when fetching from database multiplying the two should do it such as(offset(pageParam * size) and limit(size))
      const size = 7;
      const start = pageParam * size;
      const end = start + size;
      const slicedProducts = products.slice(start, end);

      return resolve(slicedProducts);
    });
  };

  return (
    <>
      <TableComponent
        columns={columns}
        queryKey={["products"]}
        queryFn={fetchProducts}
      />
      <Modal
        radius={"xl"}
        opened={opened}
        onClose={close}
        withCloseButton={false}
      >
        <Modal.Body>
          <Stack>
            <Title size="h2" fz="lg" fw="500">
              Are you sure you want to delete "{selectedProduct?.productname}"?
            </Title>
            <Flex gap=".5rem">
              <Button
                color="tailwind_gray"
                variant="outline"
                onClick={close}
                radius="xl"
                w="100%"
              >
                Discard
              </Button>
              <Button
                w="100%"
                color="red"
                radius="xl"
                onClick={() => {
                  close();
                  DeleteProductsMuation.mutate();
                }}
              >
                Delete
              </Button>
            </Flex>
          </Stack>
        </Modal.Body>
      </Modal>
    </>
  );
};

export default ProductsList;

the sorting icons " 🔼" " 🔽" visually change their appearance based on the sorting state but the columns are not being visually sorted from ascending to descending order and vice versa. I'm using "useInfiniteQuery" hook

tried other examples online, the code seems to be valid.


Solution

  • this bit was missing

      const table = useReactTable({
      columns,
      data: flatData,
      state: { sorting: sortingState, columnFilters, globalFilter },
      onSortingChange: setSortingState,
      getCoreRowModel: getCoreRowModel(),
      onColumnFiltersChange: setColumnFilters,
      onGlobalFilterChange: setGlobalFilter,
      getSortedRowModel: getSortedRowModel(),  <=========
    });