React Table
Introduction
It is a headless UI library providing the logic, state, processing and API for UI elements and interactions, but do not provide markup, styles, or pre-built implementations.
The logic includes header grouping, column filtering (searching), column sorting, column visibility, expanding, resizing, pagination
Table instance is come from of data (from api), column definition, row models
Column Defintion
Column definition is mainly 3 types:
Accessor Columns (For displaying data)
Accessor columns have an underlying data model which means they can be sorted, filtered, grouped, etc. Display Columns (For displaying customized UI)
Display columns do not have a data model which means they cannot be sorted, filtered, etc, but they can be used to display arbitrary content in the table, eg. a row actions button, checkbox, expander, etc. Grouping Columns (For grouping columns into single)
Group columns do not have a data model so they too cannot be sorted, filtered, etc, and are used to group other columns together. It's common to define a header or footer for a column group.
const columnHelper = createColumnHelper<Person>()
// Make some columns!
const defaultColumns = [
// Display Column
columnHelper.display({
id: 'actions',
cell: props => <RowActions row={props.row} />,
}),
// Grouping Column
columnHelper.group({
header: 'Name',
footer: props => props.column.id,
columns: [
// Accessor Column
columnHelper.accessor('firstName', {
cell: info => info.getValue(),
footer: props => props.column.id,
}),
// Accessor Column
columnHelper.accessor(row => row.lastName, {
id: 'lastName',
cell: info => info.getValue(),
header: () => <span>Last Name</span>,
footer: props => props.column.id,
}),
],
}),
// Grouping Column
columnHelper.group({
header: 'Info',
footer: props => props.column.id,
columns: [
// Accessor Column
columnHelper.accessor('age', {
header: () => 'Age',
footer: props => props.column.id,
}),
// Grouping Column
columnHelper.group({
header: 'More Info',
columns: [
// Accessor Column
columnHelper.accessor('visits', {
header: () => <span>Visits</span>,
footer: props => props.column.id,
}),
// Accessor Column
columnHelper.accessor('status', {
header: 'Status',
footer: props => props.column.id,
}),
// Accessor Column
columnHelper.accessor('progress', {
header: 'Profile Progress',
footer: props => props.column.id,
}),
],
}),
],
}),
]
Row Model
Row models run under the hood of TanStack Table to transform your original data in useful ways that are needed for data grid features like filtering, sorting, grouping, expanding, and pagination.
import {
getCoreRowModel,
getExpandedRowModel,
getFacetedMinMaxValues,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getGroupedRowModel,
getPaginationRowModel,
getSortedRowModel,
}
//...
const table = useReactTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
getExpandedRowModel: getExpandedRowModel(),
getFacetedMinMaxValues: getFacetedMinMaxValues(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
getFilteredRowModel: getFilteredRowModel(),
getGroupedRowModel: getGroupedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
})
Table State
To get the functional state of table
console.log("Expand", table.getState().expanded);
// {<row index>: <isExpanded>}
// Expand {16: true}
console.log("Column Filter", table.getState().columnFilters);
// [{id: <column_name>, value:<filter_value}]
// Column Filter [{id: 'title', value: 'peter'}]
console.log("Pagination", table.getState().pagination);
// {pageIndex: <pageNum - 1>, pageSize: <pageSize>}
// Pagination {pageIndex: 2, pageSize: 10}
console.log("Sorting", table.getState().sorting);
// [{id: <column_name>, desc: <isDescOrder>}]
// Sorting [{id: 'id', desc: false}]
console.log("Selection", table.getState().rowSelection);
// {<row index>: <isSelected>}
// Selection {16: true}
Implementation
Implementation, it is separated into data fetching, table definition, User interface
Data Fetching
import { useQuery } from "@tanstack/react-query";
import {
ColumnFiltersState,
PaginationState,
SortingState,
} from "@tanstack/react-table";
import axios from "axios";
import { Product, productSchema } from "../../types/my-table";
interface Props {
pagination: PaginationState;
sorting: SortingState;
filter: ColumnFiltersState;
}
export default function useMyTableQuery({
pagination,
sorting,
filter,
}: Props) {
return useQuery({
queryKey: ["products", pagination],
queryFn: async () => {
const { data: products } = await axios.get<Product[]>(
`https://fakestoreapi.com/products?pageIndex=${pagination.pageIndex}&pageSize=${pagination.pageSize}&sort=${sorting[0].id}:${sorting[0].desc ? "desc" : "asc"}&filter=${filter[0].id}:${filter[0].value}`,
);
return productSchema.array().parse(products);
},
});
}
import { z } from "zod";
export const productSchema = z.object({
id: z.number(),
title: z.string(),
price: z.number(),
category: z.string(),
});
export type Product = z.infer<typeof productSchema>;
Table Definition
import {
ColumnDef,
ColumnFiltersState,
getCoreRowModel,
getExpandedRowModel,
getFilteredRowModel,
getSortedRowModel,
PaginationState,
SortingState,
useReactTable,
} from "@tanstack/react-table";
import useMyTableQuery from "./useMyTableQuery";
import { Product } from "../..//types/my-table";
const useMyTable = (columns: ColumnDef<Product, any>[]) => {
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const [sorting, setSorting] = React.useState<SortingState>([
{
id: "id",
desc: false,
},
]);
const [filter, setFilter] = React.useState<ColumnFiltersState>([
{
id: "title",
value: "",
},
]);
const { data: product } = useMyTableQuery({
pagination,
sorting,
filter,
});
const table = useReactTable({
defaultColumn: {
size: 100, //starting column size
minSize: 50, //enforced during column resizing
maxSize: 300, //enforced during column resizing
},
columnResizeMode: "onChange",
columnResizeDirection: "ltr",
// inject data
data: product ?? [],
// inject column defintions
columns,
// inject row model
getCoreRowModel: getCoreRowModel(),
// insert react state into table status
state: { pagination, sorting, columnFilters: filter },
onPaginationChange: setPagination,
onSortingChange: setSorting,
onColumnFiltersChange: setFilter,
pageCount: 10,
manualPagination: true, // server-side
// inject functional row model for client side handling
// getPaginationRowModel: getPaginationRowModel() // client-side
getSortedRowModel: getSortedRowModel(),
// manualSorting: true,
getFilteredRowModel: getFilteredRowModel(),
// manualFiltering:true,
getRowCanExpand: () => true, // Add your logic to determine
// if a row can be expanded. True means all rows include expanded data
getExpandedRowModel: getExpandedRowModel(),
});
return { table };
};
export default useMyTable;
Context Provider
import { Table } from "@tanstack/react-table";
import { createContext } from "react";
const TableContext = createContext<{ table: Table<any> | null }>({
table: null,
});
export default TableContext;
import { useContext } from "react";
import TableContext from "../../contexts/tableProvider";
import { Table } from "@tanstack/react-table";
const useTableContext = <T>() => {
const table = useContext<{ table: Table<T> | null }>(TableContext);
return table;
};
export default useTableContext;
User Interface
Container layer (Parent component)
import { createFileRoute } from "@tanstack/react-router";
import MyTable from "../../components/myTable";
export const Route = createFileRoute("/_authenticated/my-table")({
component: () => <MyTablePage />,
});
const MyTablePage = () => {
return (
<MyTable>
<MyTable.Search />
<MyTable.Table />
<MyTable.Pagination />
</MyTable>
);
};
import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
import React from "react";
import { Product } from "../../types/my-table";
import Checkbox from "./checkBox";
import Row from "../layout/row";
import CustomTable from "./customTable";
import CustomSearch from "./customSearch";
import CustomPagination from "./customPagintaion";
import useMyTable from "../../hooks/my-table/useMyTable";
import TableContext from "../../contexts/tableProvider";
interface Props {
children: React.ReactNode;
}
const MyTable = ({ children }: Props) => {
const columnsHelper = createColumnHelper<Product>();
const columns: ColumnDef<Product, any>[] = [
columnsHelper.accessor("id", {
header: ({ header, table }) => (
<Row>
<Checkbox
{...{
checked: table.getIsAllRowsSelected(),
indeterminate: table.getIsSomeRowsSelected(),
onChange: table.getToggleAllRowsSelectedHandler(),
}}
/>
<span>ID</span>
<button
onClick={() => {
header.column.toggleSorting(
header.column.getIsSorted() !== "desc",
);
}}
>
{{
asc: "🔼",
desc: "🔽",
}[header.column.getIsSorted() as string] ?? null}
</button>
</Row>
),
cell: ({ row, getValue }) => (
<Row>
<Checkbox
{...{
checked: row.getIsSelected(),
indeterminate: row.getIsSomeSelected(),
onChange: row.getToggleSelectedHandler(),
}}
/>
<span>{getValue()}</span>
</Row>
),
sortingFn: "alphanumeric", // use built-in sorting function by name
sortUndefined: "last", //force undefined values to the end
}),
columnsHelper.accessor("title", { header: "Title" }),
columnsHelper.accessor("price", { header: "Price" }),
columnsHelper.accessor("category", { header: "Category" }),
columnsHelper.display({
id: "actions",
cell: ({ row }) => (
<button
onClick={() => {
row.getToggleExpandedHandler()();
}}
className="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
{row.getIsExpanded() ? "Collapse" : "Expand"}
</button>
),
header: "Action",
}),
];
const { table } = useMyTable(columns);
return (
<TableContext.Provider value={{ table }}>
<div className="relative overflow-x-auto shadow-md sm:rounded-lg py-10 px-5">
{children}
</div>
</TableContext.Provider>
);
};
MyTable.Search = CustomSearch;
MyTable.Table = CustomTable;
MyTable.Pagination = CustomPagination;
export default MyTable;
Presentation Layer (Child Component)
import React from "react";
import useTableContext from "../../hooks/my-table/useTableContext";
import { Product } from "../../types/my-table";
const CustomSearch = () => {
const { table } = useTableContext<Product>();
if (!table) return null;
return (
<div className="pb-4 bg-white dark:bg-gray-900">
<label htmlFor="table-search" className="sr-only">
Search
</label>
<div className="relative mt-1">
<div className="absolute inset-y-0 rtl:inset-r-0 start-0 flex items-center ps-3 pointer-events-none">
<svg
className="w-4 h-4 text-gray-500 dark:text-gray-400"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 20 20"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"
/>
</svg>
</div>
<input
type="text"
id="table-search"
className="block pt-2 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg w-80 bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Search for items"
onChange={(e) => {
table.getColumn("title")?.setFilterValue(e.target.value);
}}
/>
</div>
</div>
);
};
export default CustomSearch;
import { flexRender } from "@tanstack/react-table";
import React from "react";
import { Product } from "../../types/my-table";
import useTableContext from "../../hooks/my-table/useTableContext";
const CustomTable = () => {
const { table } = useTableContext<Product>();
if (!table) return null;
console.log("Expand", table.getState().expanded);
console.log("Column Filter", table.getState().columnFilters);
console.log("Pagination", table.getState().pagination);
console.log("Sorting", table.getState().sorting);
console.log("Selection", table.getState().rowSelection);
console.log(table.getRowModel().rows);
return (
<div style={{ direction: table.options.columnResizeDirection }}>
<table
{...{
style: {
width: table.getCenterTotalSize(),
},
}}
className="text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 border-solid border-2 w-fit"
>
<thead className="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th
key={header.id}
scope="col"
className={`p-4 relative`}
style={{ width: `${header.getSize()}px` }}
colSpan={header.colSpan}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
<div
className={`border-4 resizer ${
table.options.columnResizeDirection
} ${header.column.getIsResizing() ? "isResizing" : ""}`}
{...{
onDoubleClick: () => header.column.resetSize(),
onMouseDown: header.getResizeHandler(),
onTouchStart: header.getResizeHandler(),
}}
/>
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<React.Fragment key={row.id}>
<tr className="bg-white border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600">
{row.getVisibleCells().map((cell) => (
<td
className={`p-4 `}
style={{ width: `${cell.column.getSize()}px` }}
key={cell.id}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
{row.getIsExpanded() && (
<tr>
<td colSpan={row.getAllCells().length}>
{row.getValue("title")}
</td>
</tr>
)}
</React.Fragment>
))}
</tbody>
</table>
</div>
);
};
export default CustomTable;
import React from "react";
import useTableContext from "../../hooks/my-table/useTableContext";
import { Product } from "../../types/my-table";
const CustomPagination = () => {
const { table } = useTableContext<Product>();
if (!table) return null;
const getPagination = (totalPages: number, currentPage: number): number[] => {
const pageNumbers = [];
const maxVisiblePages = 5;
const halfVisible = Math.floor(maxVisiblePages / 2);
let startPage = Math.max(1, currentPage - halfVisible);
let endPage = Math.min(totalPages, currentPage + halfVisible);
if (endPage - startPage < maxVisiblePages - 1) {
if (startPage === 1) {
endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
} else if (endPage === totalPages) {
startPage = Math.max(1, endPage - maxVisiblePages + 1);
}
}
for (let i = startPage; i <= endPage; i++) {
pageNumbers.push(i);
}
return pageNumbers;
};
const currentPage = table.getState().pagination.pageIndex + 1;
return (
<nav
className="flex items-center flex-column flex-wrap md:flex-row justify-between pt-4"
aria-label="Table navigation"
>
<span className="text-sm font-normal text-gray-500 dark:text-gray-400 mb-4 md:mb-0 block w-full md:inline md:w-auto">
Page of{" "}
<span className="font-semibold text-gray-900 dark:text-white">
{table.getState().pagination.pageIndex + 1}
</span>{" "}
of{" "}
<span className="font-semibold text-gray-900 dark:text-white">
{table.getPageCount()}
</span>
</span>
<ul className="inline-flex -space-x-px rtl:space-x-reverse text-sm h-8">
<li>
<button
className="flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 bg-white border border-gray-300 rounded-s-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
onClick={() => {
table.setPageIndex(0);
}}
>
{"<<"}
</button>
</li>
<li>
<button
disabled={!table.getCanPreviousPage()}
onClick={() => {
table.previousPage();
}}
className="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
>
{"<"}
</button>
</li>
{getPagination(table.getPageCount(), currentPage).map((pageNum) => (
<li key={pageNum}>
<button
className={`${currentPage === pageNum ? " font-bold" : ""} flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white`}
onClick={() => {
table.setPageIndex(pageNum - 1);
}}
>
{pageNum}
</button>
</li>
))}
<li>
<button
className="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
disabled={!table.getCanNextPage()}
onClick={() => {
table.nextPage();
}}
>
{">"}
</button>
</li>
<li>
<button
disabled={!table.getCanNextPage()}
className="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 rounded-e-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
onClick={() => {
table.setPageIndex(table.getPageCount() - 1);
}}
>
{">>"}
</button>
</li>
</ul>
<select
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
value={table.getState().pagination.pageSize}
onChange={(e) => {
console.log(e.target.value);
table.setPageSize(Number(e.target.value));
}}
>
{[10, 20, 30, 40, 50].map((pageSize) => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</nav>
);
};
export default CustomPagination;
import React, { HTMLProps } from "react";
function Checkbox({
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 +
"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600 cursor-pointer"
}
{...rest}
/>
);
}
export default Checkbox;
Last updated
Was this helpful?