Skip to content

Instantly share code, notes, and snippets.

@MansourM61
Last active January 15, 2026 14:46
Show Gist options
  • Select an option

  • Save MansourM61/bc040a7545f27250bb67b2ccddf5f65d to your computer and use it in GitHub Desktop.

Select an option

Save MansourM61/bc040a7545f27250bb67b2ccddf5f65d to your computer and use it in GitHub Desktop.

Table

TanStack Table

TanStack Table is a Headless UI library for building powerful tables & datagrids. The examples are all in React.

To install:

npm install @tanstack/react-table

Note

The recommendation from TanStack official website is to use useMemo hook when using the tables. However, the compiler complains about useMemo and suggests inserting "use no memo"; as the first line of the component function. Even with this solution, the error still exists. Therefore, based on the TanStack recommandation, useMemo will be used.

Table with Sorting and Formatting

import {
  useReactTable,
  getCoreRowModel,
  flexRender,
  getSortedRowModel,
  type SortingState,
  type ColumnFiltersState,
  type Column,
  type Row,
  type ColumnDef,
} from "@tanstack/react-table";
import { useMemo, useState } from "react";
import { format } from "date-fns";

// Data table from https://www.mockaroo.com/

// Column Defs: Objects used to configure a column and its data model, display templates, and more
// flat columns
const COLUMNS = [
  {
    header: "Id", // label to show in the header
    footer: "Id", // label to show in the footer
    accessorKey: "id", // name of the columns in the data source
  },
  // ...
  {
    header: "Date of Birth",
    footer: "Date of Birth",
    accessorKey: "date_of_birth",
    cell: (props: object & { getValue: () => string }) => {
      return format(new Date(props.getValue()), "dd/MM/yyyy");
    },
  },
  // ...
];

// grouped columns
const GROUPED_COLUMNS = [
  {
    header: "Id",
    footer: "Id",
    accessorKey: "id",
  },
  {
    header: "Name",
    footer: "Name",
    columns: [
      {
        header: "First Name",
        footer: "First Name",
        accessorKey: "first_name",
      },
      {
        header: "Last Name",
        footer: "Last Name",
        accessorKey: "last_name",
      },
    ],
  },
  // ...
];

export default function Component() {
  // "use no memo";
  // the table hook gives an error and this solution is suggested (it does not remove the error though)

  // `useMemo` hook is used to avoid unnecessary re-renders
  const data = useMemo(() => TABLE_DATA, []);
  const columns = useMemo(() => COLUMNS, []); // for flat columns
  const columns = useMemo(() => GROUPED_COLUMNS, []); // for grouped columns

  const [sorting, setSorting] = useState<SortingState>([]);

  const {
    getHeaderGroups, // Header groups are computed slices of nested header levels, each containing a group of headers
    getRowModel, // Row data with each row mirroring its respective row data and provides row-specific APIs
    getFooterGroups, // the same as `getHeaderGroups` but for the footer
  } = useReactTable({
    data, // input data: an array of objects whit property of each object having the same name as `accessorKey` in column definition array
    columns, // columns definition responsible for accessors, display and grouping of columns
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
    onSortingChange: setSorting,
    state: {
      sorting,
    },
  });

  return (
    <div>
      <table>
        <thead>
          {getHeaderGroups().map((headerGroup) => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map((header) => {
                return (
                  <th key={header.id} colSpan={header.colSpan}>
                    {header.isPlaceholder ? null : (
                      <div
                        className={
                          header.column.getCanSort()
                            ? "cursor-pointer select-none"
                            : ""
                        }
                        onClick={header.column.getToggleSortingHandler()}
                        title={
                          header.column.getCanSort()
                            ? header.column.getNextSortingOrder() === "asc"
                              ? "Sort ascending"
                              : header.column.getNextSortingOrder() === "desc"
                              ? "Sort descending"
                              : "Clear sort"
                            : undefined
                        }
                      >
                        {flexRender(
                          header.column.columnDef.header,
                          header.getContext()
                        )}
                        {{
                          asc: " 🔼",
                          desc: " 🔽",
                        }[header.column.getIsSorted() as string] ?? null}
                      </div>
                    )}
                  </th>
                );
              })}
            </tr>
          ))}
        </thead>
        <tbody>
          {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>
        <tfoot>
          {getFooterGroups().map((footerGroup) => (
            <tr key={footerGroup.id}>
              {footerGroup.headers.map((footer) => (
                <td key={footer.id} colSpan={footer.colSpan}>
                  {footer.isPlaceholder
                    ? null
                    : flexRender(
                        footer.column.columnDef.header,
                        footer.getContext()
                      )}
                </td>
              ))}
            </tr>
          ))}
        </tfoot>
      </table>
    </div>
  );
}

Global and Column Filtering

import {
  useReactTable,
  getCoreRowModel,
  flexRender,
  getFilteredRowModel,
  type ColumnFiltersState,
  type Column,
  type Row,
  type ColumnDef,
} from "@tanstack/react-table";

function GlobalFilter({
  filter,
  setFilter,
}: {
  filter: string,
  setFilter: (x: string) => void,
}) {
  return (
    <span>
      Global Search in all Columns:{" "}
      <input
        type="text"
        value={filter || ""}
        onChange={(e) => {
          setFilter(e.target.value);
        }}
      />
    </span>
  );
}

function ColumnFilter({
  column,
}: {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  column: Column<any, unknown>;
}) {
  const columnFilterValue = column.getFilterValue() as string;
  const columnSetFilter = column.setFilterValue;

  return (
    <span>
      Search in the Column:{" "}
      <input
        type="text"
        value={columnFilterValue || ""}
        onChange={(e) => columnSetFilter(e.target.value)}
      />
    </span>
  );
}

// Row type (contains all columns)
type DataRow = {
  id: number;
  first_name: string;
  last_name: string;
  email: string;
  date_of_birth: string;
  age: number;
  country: string;
  phone: string;
};

// defining column definitions
const COLUMNS: ColumnDef<DataRow>[] = [
  {
    header: "Id",
    footer: "Id",
    accessorKey: "id",
    filterFn: (row: Row<DataRow>, columnId: string, filterValue: unknown) => {
      return (
        (row.getValue(columnId) as object).toString().trim() ===
        String(filterValue)
      );
    },  // custom column filtering function
  },
  {
    header: "First Name",
    footer: "First Name",
    accessorKey: "first_name",
    filterFn: "includesString",  // standard column filtering function
  },
  // ...
  {
    header: "Phone",
    footer: "Phone",
    accessorKey: "phone",
    filterFn: "inNumberRange",
    enableColumnFilter: false,  // exclude this column from column filtering
  },
];

export default function Component() {
  // "use no memo";
  // the table hook gives an error and this solution is suggested (it does not remove the error though)

  // `useMemo` hook is used to avoid unnecessary re-renders
  const data = useMemo(() => RAW_DATA, []);
  const columns = useMemo(() => COLUMNS, []); // for flat columns

  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]); // can set initial column filter state here

  const {
    getHeaderGroups,
    getRowModel,
    getFooterGroups,
    setGlobalFilter,  // callback to set global filter state (search phrase)
    getState,
  } = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
    getFilteredRowModel: getFilteredRowModel(), // needed for client side filtering
    globalFilterFn: "includesString", // built-in filter function
    state: {
      columnFilters,
    },
    onColumnFiltersChange: setColumnFilters,
  });

  const globalFilter = getState().globalFilter;  // global filter state (search phrase)

  return (
    <div>
      <GlobalFilter filter={globalFilter} setFilter={setGlobalFilter} />
      <table>
        <thead>
          {getHeaderGroups().map((headerGroup) => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map((header) => (
                <th key={header.id} colSpan={header.colSpan}>
                  {header.isPlaceholder
                    ? null
                    : flexRender(
                        header.column.columnDef.header,
                        header.getContext()
                      )}
                  <div>
                    {header.column.getCanFilter() ? (
                      <div>
                        <ColumnFilter column={header.column} />
                      </div>
                    ) : null}
                  </div>
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {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>
        <tfoot>
          {getFooterGroups().map((footerGroup) => (
            <tr key={footerGroup.id}>
              {footerGroup.headers.map((footer) => (
                <td key={footer.id} colSpan={footer.colSpan}>
                  {footer.isPlaceholder
                    ? null
                    : flexRender(
                        footer.column.columnDef.header,
                        footer.getContext()
                      )}
                </td>
              ))}
            </tr>
          ))}
        </tfoot>
      </table>
    </div>
  );
}

Table with Pagination

import {
  useReactTable,
  getCoreRowModel,
  flexRender,
  getPaginationRowModel,
} from "@tanstack/react-table";
import { useMemo } from "react";

const COLUMNS = [
  {
    header: "Id",
    accessorKey: "id",
  },
  //...
];

export default function Component() {
  // "use no memo";
  // the table hook gives an error and this solution is suggested (it does not remove the error though)

  const data = useMemo(() => TABLE_DATA, []);
  const columns = useMemo(() => COLUMNS, []);

  const {
    getHeaderGroups,
    getRowModel,
    nextPage, // callback to go to next page
    previousPage, // callback to go to previous page
    getCanNextPage, // callback to find out if next page is available
    getCanPreviousPage, // callback to find out if previous page is available
    setPageIndex, // callback to set page index
    getPageCount, // callback to get total number of pages
    setPageSize, // callback to set the page size
    getState, // callback to get table state
  } = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(), //load client-side pagination code
    initialState: {
      pagination: {
        pageIndex: 0, // custom initial page index
        pageSize: 10,
      },
    },
  });

  const pageIndex = getState().pagination.pageIndex; // get current page index
  const pageSize = getState().pagination.pageSize; // get current page size

  return (
    <div>
      <table>
        <thead>
          {getHeaderGroups().map((headerGroup) => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map((header) => (
                <th key={header.id} colSpan={header.colSpan}>
                  {header.isPlaceholder
                    ? null
                    : flexRender(
                        header.column.columnDef.header,
                        header.getContext()
                      )}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {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>
        <span>
          Page{" "}
          <strong>
            {pageIndex + 1} of {getPageCount()}
          </strong>{" "}
        </span>
        <span>
          | Go to page:{" "}
          <input
            type="number"
            defaultValue={pageIndex + 1}
            onChange={(e) => {
              const pageNumber = e.target.value
                ? Number(e.target.value) - 1
                : 0;
              setPageIndex(pageNumber);
            }}
            style={{ width: "50px" }}
          />
        </span>

        <select
          value={pageSize}
          onChange={(e) => setPageSize(Number(e.target.value))}
        >
          {[10, 25, 50].map((pageSize) => (
            <option key={pageSize} value={pageSize}>
              Show {pageSize} items
            </option>
          ))}
        </select>
        <button
          onClick={() => setPageIndex(0)}
          disabled={!getCanPreviousPage()}
        >
          {"<<"}
        </button>
        <button onClick={() => previousPage()} disabled={!getCanPreviousPage()}>
          Previous
        </button>
        <button onClick={() => nextPage()} disabled={!getCanNextPage()}>
          Next
        </button>
        <button
          onClick={() => setPageIndex(getPageCount() - 1)}
          disabled={!getCanNextPage()}
        >
          {">>"}
        </button>
      </div>
    </div>
  );
}

Table with Row Selection

import {
  useReactTable,
  getCoreRowModel,
  flexRender,
  type RowSelectionState,
} from "@tanstack/react-table";
import { useMemo, useState } from "react";

import type { HTMLProps } from "react";
import React from "react";

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

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

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

const COLUMNS = [
  {
    id: "select",
    header: ({ table }) => (
      <IndeterminateCheckbox
        {...{
          checked: table.getIsAllRowsSelected(),
          indeterminate: table.getIsSomeRowsSelected(),
          onChange: table.getToggleAllRowsSelectedHandler(),
        }}
      />
    ),
    cell: ({ row }) => (
      <div className="px-1">
        <IndeterminateCheckbox
          {...{
            checked: row.getIsSelected(),
            disabled: !row.getCanSelect(),
            indeterminate: row.getIsSomeSelected(),
            onChange: row.getToggleSelectedHandler(),
          }}
        />
      </div>
    ),
  },
  {
    header: "Id",
    footer: "Id",
    accessorKey: "id",
  },
  // ...
];

export default function Component() {
  // "use no memo";
  // the table hook gives an error and this solution is suggested (it does not remove the error though)

  const [rowSelection, setRowSelection] = useState<RowSelectionState>({}); // manage your own row selection state

  const data = useMemo(() => TABLE_DATA, []);
  const columns = useMemo(() => COLUMNS, []);

  const { getHeaderGroups, getRowModel, getFooterGroups } = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
    onRowSelectionChange: setRowSelection, //hoist up the row selection state to your own scope
    state: {
      rowSelection, // pass the row selection state back to the table instance
    },
  });

  return (
    <div>
      <table>
        <thead>
          {getHeaderGroups().map((headerGroup) => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map((header) => (
                <th key={header.id} colSpan={header.colSpan}>
                  {header.isPlaceholder
                    ? null
                    : flexRender(
                        header.column.columnDef.header,
                        header.getContext()
                      )}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {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>
        <tfoot>
          {getFooterGroups().map((footerGroup) => (
            <tr key={footerGroup.id}>
              {footerGroup.headers.map((footer) => (
                <td key={footer.id} colSpan={footer.colSpan}>
                  {footer.isPlaceholder
                    ? null
                    : flexRender(
                        footer.column.columnDef.header,
                        footer.getContext()
                      )}
                </td>
              ))}
            </tr>
          ))}
        </tfoot>
      </table>
      <pre>
        <code>
          {JSON.stringify({
            rowSelection: rowSelection,
          })}
        </code>
      </pre>
    </div>
  );
}

Table with Column Ordering and Hiding

import {
  useReactTable,
  getCoreRowModel,
  flexRender,
} from "@tanstack/react-table";
import { useMemo, useState } from "react";

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

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

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

const COLUMNS = [
  {
    header: "Id",
    footer: "Id",
    accessorKey: "id",
  },
  //...
];

export default function Component() {
  // "use no memo";
  // the table hook gives an error and this solution is suggested (it does not remove the error though)

  const data = useMemo(() => TABLE_DATA, []);
  const columns = useMemo(() => COLUMNS, []);

  const [columnOrder, setColumnOrder] = useState<string[]>(
    columns.map((c) => c.accessorKey)
  ); // optionally initialize the column order

  // initial column visibility
  const [columnVisibility, setColumnVisibility] = useState(
    columns
      .map((c) => ({ [c.accessorKey]: true }))
      .reduce((acc, curr) => ({ ...acc, ...curr }))
  );

  const {
    getHeaderGroups,
    getRowModel,
    getFooterGroups,
    getIsAllColumnsVisible, // callback function to show if all columns are visible (used to manage all columns visibility)
    getToggleAllColumnsVisibilityHandler, // handler for toggling the visibility of all columns (used to manage all columns visibility)
    getAllLeafColumns, // returns all leaf-node columns in the table flattened to a single level.
  } = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
    onColumnOrderChange: setColumnOrder, // callback to set the order of columns
    onColumnVisibilityChange: setColumnVisibility, // callback to change the columns visibility (used to manage individual column visibility)
    state: {
      columnOrder, // initial order of columns (array containing the `accessorKey` of columns in desired order)
      columnVisibility, // initial status of columns visibilities in the format of [ { [accessorKey]: false/true } ]
    },
  });

  // a simple function to set the columns orders
  const changeOrder = () => {
    const newOrder = [...columnOrder.slice(1), columnOrder[0]];
    setColumnOrder(newOrder);
  };

  return (
    <div>
      <button onClick={changeOrder}>Change column order</button>
      <IndeterminateCheckbox
        {...{
          checked: getIsAllColumnsVisible(),
          onChange: getToggleAllColumnsVisibilityHandler(),
        }}
      />
      Toggle All
      {getAllLeafColumns().map((column) => {
        return (
          <div key={column.id} className="px-1">
            <label>
              <input
                {...{
                  type: "checkbox",
                  checked: column.getIsVisible(),
                  onChange: column.getToggleVisibilityHandler(),
                }}
              />{" "}
              {column.id}
            </label>
          </div>
        );
      })}
      <table>
        <thead>
          {getHeaderGroups().map((headerGroup) => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map((header) => (
                <th key={header.id} colSpan={header.colSpan}>
                  {header.isPlaceholder ? null : (
                    <div>
                      <IndeterminateCheckbox
                        {...{
                          checked: header.column.getIsVisible(),
                          onChange: header.column.getToggleVisibilityHandler(),
                        }}
                      />
                      {flexRender(
                        header.column.columnDef.header,
                        header.getContext()
                      )}
                    </div>
                  )}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {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>
        <tfoot>
          {getFooterGroups().map((footerGroup) => (
            <tr key={footerGroup.id}>
              {footerGroup.headers.map((footer) => (
                <td key={footer.id} colSpan={footer.colSpan}>
                  {footer.isPlaceholder
                    ? null
                    : flexRender(
                        footer.column.columnDef.header,
                        footer.getContext()
                      )}
                </td>
              ))}
            </tr>
          ))}
        </tfoot>
      </table>
    </div>
  );
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment