TanStack Table is a Headless UI library for building powerful tables & datagrids. The examples are all in React.
To install:
npm install @tanstack/react-tableNote
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.
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>
);
}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>
);
}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>
);
}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>
);
}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>
);
}