A table is an arrangement that organizes any information into rows and columns—it stores and displays data in a structured and clear format that everyone can understand. For example, when you want to display dashboard statistics, a list of details such as staff details, products, students, and lots more.
Implementing tables in React can get cumbersome and technical, especially when you need to perform basic table functionalities like sorting, filtering, pagination, and lots more.
#What is React Table?
Tanstack table is a free open-source headless library with around 25K stars on GitHub. It allows you to build tables and has individual libraries to support different frameworks like React, Vue, Angular, Solid, etc.
Tanstack React Table or React Table library is part of the tanstack table that allows us to build tables in React applications. It gives you an API to build tables and do different operations on them, it follows a headless UI approach and allows you to fully build your custom UI. It is also very flexible to use.
React table uses hooks to help build tables, giving you complete control over how you want the UI to appear and function (Headless UI). These hooks are lightweight, fast, fully customizable, and flexible but do not render any markup or styles for you. React Table is used by many well-known tech companies due to its flexibility and easy-to-implement features. These features, such as sorting, pagination, and filtering, are implemented with hooks.
#React table features
React Table has some fantastic features that make it easy to build tables. These features include:
- It is lightweight, about 5kb - 14kb+.
- Headless design, manage state and hooks for all functionalities, use your own UI
- It is fully customizable (JSX, templates, state, styles, callbacks).
- TypeScript support.
- Does not influence data calls (it does not perform data calls; it just uses passed-in data).
- Support for table functionalities like filtering, sorting, pagination, and many more.
#Should you use React Table?
You can check these points to see if you should be using the React Table library
The current UI framework does not provide a Table component
If your current UI framework does not provide a table component and you need to add a table, then you can consider React Table to manage all tables in your application. UI Frameworks like Material-UI or AntD would already have a Table component, in those cases maybe checking if those components fulfill your requirements would make more sense.
Headless Solution
If you want full control over the table's look and feel without being tied to any specific UI or styling framework. React Table’s headless design seamlessly integrates CSS and different UI libraries. Again, if your UI library already has a Table component that works for your use cases, it does not make sense to add extra overhead with an additional library.
Other Considerations
- It is highly customizable and provides an extensive API for all table-related operations.
- It has strong community support.
- It is lightweight, fast, and comes with proper TypeScript support.
- It has an intermediate learning curve and will need some time for API exploration and using various features.
#Building a Table
In this article, we will fetch some data from Hygraph via GraphQL, and then display that data in a table using the @tanstack/react-table library. Also, we will implement client-side sorting, filtering, and pagination, and finally, we will see how to extend our table component to display any kind of data. In our Hygraph project, we have created a model and populated it with some mock employee data. Hygraph is a headless CMS that enables teams to distribute content to any channel. If this is your first time learning about Hygraph, establish a free-forever developer account.
Base setup
In a situation where you have successfully added all your data to hygraph, or you have your content API ready, we can now fetch these data into our application and store it in a state.
To fetch our data from Hygraph, we use GraphQL using the graphql-request library. We can install this library using the command below in our project directory. Also, we will be using TanStack table’s React library to build our table. We will need to install this library as well.
npm i graphql-requestnpm i @tanstack/react-table
Getting data from Hygraph
Once the above installation is done, we can navigate to a fresh component to create our table. We will use useState()
to store our data and the useEffect()
hook to fetch our data from Hygraph.
Let us declare types for our data first
types.ts
export interface IEmployee {id: number,firstName: string;lastName: string;email: string;department: string;dateJoined: string;}
Create a file to store our API requests.
api.ts
import request from "graphql-request";import { HYGRAPH_URL } from "./utils/constants";export const getTableData = async () => {const response = await request(HYGRAPH_URL,`{employees (first:50) {id_numberfirstNamelastNamedepartmentdateJoined}}`);return response;};
Use the API request in our component to fetch the data
ReactTable.tsx
import { getTableData } from "../../api.ts";import { useState, useEffect } from "react";import { IEmployee } from "../../utils/types.ts";export const ReactTable = () => {const [data, setData] = useState<IEmployee[]>([]);useEffect(() => {const fetchData = async () => {const { employees } = await getTableData();setData(employees);};fetchData();}, []);return <div>...</div>;};
Column definition
When using React tables, data and columns are the two significant pieces of information needed. The information we will pass into the rows is known as the data, and the objects used to define the table columns are called the columns (headers, rows, how we will show the row, etc.). We will create columns in a separate file and then import them into our component.
columns.ts
import { ColumnDef } from "@tanstack/react-table";import { IEmployee } from "../../utils/types";export const COLUMNS: ColumnDef<IEmployee>[] = [{header: "ID",accessorKey: "id_number",},{header: "First Name",accessorKey: "firstName",},{header: "Last Name",accessorKey: "lastName",},{header: "Email",accessorKey: "email",},{header: "Department",accessorKey: "department",},{header: "Date Joined",accessorKey: "dateJoined",},];
The columns array consists of objects representing single columns in our table. Currently, we have specified the configuration of these objects with two items:
header
: This is the column's header, which will be displayed at the top of each column.accessorKey
: this is the key from our data that will be used to assign value to a column.
There can be more configuration added to this column object, for example, this is how one row of our data coming from Hygraph looks like:
{id: 1,firstName: "Tandi",lastName: "Withey",email: "twithey0@arizona.edu",department: "Marketing",dateJoined: "2014-10-23T07:29:35Z",}
The dateJoined
column value like this 2014-10-23T07:29:35Z
will look bad and unreadable in the table with all the hour, minute, and second data. We can use the cell
property to display the data however we want as shown below:
...{header: "Date Joined",accessorKey: "dateJoined",cell: (info) => {const value = (info.getValue() as string).substring(0,10)return value}}...
Next, let's say instead of displaying firstName
and lastName
as two different columns we want to combine data into a single column, we can do it using the accessorFn
property as shown below:
...{header: "Name",accessorFn: (row) => `${row.firstName} ${row.lastName}`,}...
These are some basic use cases, for custom requirements you can further explore the column definition API in detail here
The Table instance
React table makes use of hooks in its headless approach. The first hook we will use is the useReactTable
hook to instantiate our table so we can access all the methods needed to create our table.
The useReactTable
hook requires at least columns
, data,
and getCoreRowModel
to be passed to it as an argument. We should also use the useMemo hook to help memoize our columns array to avoid unnecessary re-renders.
ReactTable.tsx
// other imports ...import { IEmployee } from "../../../utils/types.ts";import { COLUMNS } from "./columns.ts";import { getCoreRowModel, useReactTable } from "@tanstack/react-table";export const ReactTable = () => {const [data, setData] = useState<IEmployee[]>([]);// useEffect() to fetch data ...const columns = useMemo(() => COLUMNS, []);const table = useReactTable({columns,data,getCoreRowModel: getCoreRowModel(),});return <div>...</div>;};
Markup for the table
From the table instance table
above we can get functions like getHeaderGroups
, getRowModel
, and getFooterGroups,
these functions can be used to form our table markup.
Let's destructure directly to access these props:
ReactTable.tsx
...const { getHeaderGroups, getRowModel, getFooterGroups } = table;...
For table markup, we can use HTML tags like table
, thead,tbody
, tfoot
, th
, tr
, and td
. We are using Chakra for this example but feel free to use any UI framework of your choice.
ReactTable.tsx
import {useReactTable,getCoreRowModel,flexRender,} from "@tanstack/react-table";import {Table,TableContainer,Tbody,Td,Tfoot,Th,Thead,Tr,} from "@chakra-ui/react";export const ReactTable = () => {// all existing logic code ...const { getHeaderGroups, getRowModel, getFooterGroups } = table;return (<TableContainer p={8}><Heading>React Table Example</Heading><Table><Thead>{getHeaderGroups().map((headerGroup) => (<Tr key={headerGroup.id}>{headerGroup.headers.map((header) => (<Th key={header.id}>{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((header) => (<Th key={header.id}>{flexRender(header.column.columnDef.footer, header.getContext())}</Th>))}</Tr>))}</Tfoot></Table></TableContainer>);};
At this point, when we check our browser, our table will look like this
Pretty cool, pat your back if you got till here 😎
#Basic table operations
We can use React Table’s API to perform common table-related operations like filtering, pagination, and sorting. Ideally, for very huge datasets these operations happen on the server side. However, for this article and understanding React Table’s API we will demonstrate how to do basic filtering, pagination, and sorting using the React Table API.
Filtering
For filtering the table data first we will need to set up a state variable and capture the user input for filtering the data
// ... existing code …const [globalFilter, setGlobalFilter] = useState("");// ... existing code ...<Inputvalue={globalFilter || ""}onChange={(e) => setGlobalFilter(e.target.value)}placeholder="Search all columns..."my={4}/>// ... existing code ...
We will need to import getFilteredRowModel
and pass it in the react table object initialization. Also, we will need to pass our state globalFilter
and its function setGlobalFilter
while creating the table instance as shown below:
import { getFilteredRowModel } from "@tanstack/react-table";const table = useReactTable({// ... existing code ...state: {globalFilter,},getFilteredRowModel: getFilteredRowModel(),onGlobalFilterChange: setGlobalFilter,});
Below is all the combined code that we will need for implementing table filtering.
// ... existing code ...import {// ... existing code ...getFilteredRowModel,} from "@tanstack/react-table";export const ReactTable = () => {// ... existing code ...const [globalFilter, setGlobalFilter] = useState("");// ... existing code ...const table = useReactTable({columns,data,state: {globalFilter,},getCoreRowModel: getCoreRowModel(),getFilteredRowModel: getFilteredRowModel(),onGlobalFilterChange: setGlobalFilter,});const { getHeaderGroups, getRowModel, getFooterGroups } = table;return (<TableContainer><Inputvalue={globalFilter || ""}onChange={(e) => setGlobalFilter(e.target.value)}placeholder="Search all columns..."my={4}/>// ... existing code ...)
Pagination
Similarly, for pagination, we will need to import getPaginationRowModel
and pass it in the table instance initialization.
// ... existing code …import { getPaginationRowModel } from "@tanstack/react-table";// ... existing code ...const table = useReactTable({// ... existing code ...getPaginationRowModel: getPaginationRowModel(),});
That’s it, now we can use react table’s API and build some basic pagination functionality like going to the previous page, next page, first page, and last page. First, we need to destructure the required utility functions from the table instance as shown below
const {// ... existing code ...firstPage,previousPage,lastPage,nextPage,getCanNextPage,getCanPreviousPage,} = table;
firstPage
will allow us to navigate to the first page, lastPage
will send the user to the last page of the table, getCanPreviousPage
will return a boolean value if the user can go to the previous page, previousPage
will navigate the user to the previous page, and similarly, getCanNextPage
will return a boolean value if the user can go to the next page and nextPage
will move current view to the next page.
Finally, we can build the necessary UI at the bottom of the table for the user to navigate.
// ... existing code …import {// ... existing code ...HStack,IconButton,Heading,} from "@chakra-ui/react";import {ArrowBackIcon,ArrowForwardIcon,ArrowLeftIcon,ArrowRightIcon,} from "@chakra-ui/icons";// ... existing code ...<HStack align="center" justify="center" m={4}><IconButtonicon={<ArrowLeftIcon />}onClick={() => firstPage()}aria-label="">First Page</IconButton><IconButtonicon={<ArrowBackIcon />}isDisabled={!getCanPreviousPage()}onClick={() => previousPage()}aria-label="">Prev Page</IconButton><IconButtonicon={<ArrowForwardIcon />}isDisabled={!getCanNextPage()}onClick={() => nextPage()}aria-label="">Next Page</IconButton><IconButtonicon={<ArrowRightIcon />}onClick={() => lastPage()}aria-label="">Last Page</IconButton></HStack>// ... existing code ...
Below is all the combined code that we will need for implementing table pagination.
import {// ... existing code ...getPaginationRowModel,} from "@tanstack/react-table";import {// ... existing code ...HStack,IconButton,Heading,} from "@chakra-ui/react";import {ArrowBackIcon,ArrowForwardIcon,ArrowLeftIcon,ArrowRightIcon,} from "@chakra-ui/icons";export const ReactTable = () => {// ... existing code ...const table = useReactTable({// ... existing code ...getPaginationRowModel: getPaginationRowModel(),});const {getHeaderGroups,getRowModel,firstPage,previousPage,lastPage,nextPage,getCanNextPage,getCanPreviousPage,} = table;return (<TableContainer p={8}><Heading>React Table Example</Heading><Inputvalue={globalFilter || ""}onChange={(e) => setGlobalFilter(e.target.value)}placeholder="Search all columns..."my={4}/><Table>// ... existing code ...</Table><HStack align="center" justify="center" m={4}><IconButtonicon={<ArrowLeftIcon />}onClick={() => firstPage()}aria-label="">First Page</IconButton><IconButtonicon={<ArrowBackIcon />}isDisabled={!getCanPreviousPage()}onClick={() => previousPage()}aria-label="">Prev Page</IconButton><IconButtonicon={<ArrowForwardIcon />}isDisabled={!getCanNextPage()}onClick={() => nextPage()}aria-label="">Next Page</IconButton><IconButtonicon={<ArrowRightIcon />}onClick={() => lastPage()}aria-label="">Last Page</IconButton></HStack></TableContainer>);}
Sorting
To add sorting functionality to our table, we will need to create a state variable for storing the sorting state. We will then need to pass the sorting state sorting
and its setter function setSorting
and getSortedRowModel
while creating the table instance.
// ... existing code ...import { getSortedRowModel, SortingState } from "@tanstack/react-table";export const ReactTable = () => {// ... existing code ...const [sorting, setSorting] = useState<SortingState>([]);// ... existing code ...const table = useReactTable({// ... existing code ...state: {globalFilter,sorting,},getSortedRowModel: getSortedRowModel(),onSortingChange: setSorting,});
Now we can add an onClick
event to the header of each column which will toggle the sorting between ascending, descending, and none as shown below.
<Thead>{getHeaderGroups().map((headerGroup) => (<Tr key={headerGroup.id}>{headerGroup.headers.map((header) => (<Thkey={header.id}onClick={header.column.getToggleSortingHandler()}>{header.isPlaceholder? null: flexRender(header.column.columnDef.header,header.getContext())}{header.column.getIsSorted() === "asc" && <ArrowUpIcon />}{header.column.getIsSorted() === "desc" && <ArrowDownIcon />}</Th>))}</Tr>))}</Thead>
Below is all the combined code that we will need for implementing table sorting.
// ... existing code ...import {// ... existing code ...getSortedRowModel,SortingState,} from "@tanstack/react-table";export const ReactTable = () => {// ... existing code ...const [sorting, setSorting] = useState<SortingState>([]);// ... existing code ...const table = useReactTable({// ... existing code ...state: {globalFilter,sorting,},getSortedRowModel: getSortedRowModel(),onSortingChange: setSorting,});return (// ... existing code ...<Thead>{getHeaderGroups().map((headerGroup) => (<Tr key={headerGroup.id}>{headerGroup.headers.map((header) => (<Thkey={header.id}onClick={header.column.getToggleSortingHandler()}>{header.isPlaceholder? null: flexRender(header.column.columnDef.header,header.getContext())}{header.column.getIsSorted() === "asc" && <ArrowUpIcon />}{header.column.getIsSorted() === "desc" && <ArrowDownIcon />}</Th>))}</Tr>))}</Thead>// ... existing code ...)
After combining all these functionalities of filtering, sorting and pagination here is how our final component looks like
import {ArrowBackIcon,ArrowDownIcon,ArrowForwardIcon,ArrowLeftIcon,ArrowRightIcon,ArrowUpIcon,} from "@chakra-ui/icons";import {Heading,HStack,IconButton,Input,Table,TableContainer,Tbody,Td,Th,Thead,Tr,} from "@chakra-ui/react";import {flexRender,getCoreRowModel,getFilteredRowModel,getPaginationRowModel,getSortedRowModel,SortingState,useReactTable,} from "@tanstack/react-table";import { useEffect, useMemo, useState } from "react";import { getTableData } from "../../../api.ts";import { IEmployee } from "../../../utils/types.ts";import { COLUMNS } from "./columns.ts";export const ReactTable = () => {const columns = useMemo(() => COLUMNS, []);const [data, setData] = useState<IEmployee[]>([]);const [globalFilter, setGlobalFilter] = useState("");const [sorting, setSorting] = useState<SortingState>([]);useEffect(() => {const fetchData = async () => {const { datasets } = await getTableData();setData(datasets);console.log(datasets);};fetchData();}, []);const table = useReactTable({columns,data,state: {globalFilter,sorting,},getCoreRowModel: getCoreRowModel(),getFilteredRowModel: getFilteredRowModel(),onGlobalFilterChange: setGlobalFilter,getPaginationRowModel: getPaginationRowModel(),getSortedRowModel: getSortedRowModel(),onSortingChange: setSorting,});const {getHeaderGroups,getRowModel,firstPage,previousPage,lastPage,nextPage,getCanNextPage,getCanPreviousPage,} = table;return (<TableContainer p={8}><Heading>React Table Example</Heading><Inputvalue={globalFilter || ""}onChange={(e) => setGlobalFilter(e.target.value)}placeholder="Search all columns..."my={4}/><Table><Thead>{getHeaderGroups().map((headerGroup) => (<Tr key={headerGroup.id}>{headerGroup.headers.map((header) => (<Thkey={header.id}onClick={header.column.getToggleSortingHandler()}>{header.isPlaceholder? null: flexRender(header.column.columnDef.header,header.getContext())}{header.column.getIsSorted() === "asc" && <ArrowUpIcon />}{header.column.getIsSorted() === "desc" && <ArrowDownIcon />}</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><HStack align="center" justify="center" m={4}><IconButtonicon={<ArrowLeftIcon />}onClick={() => firstPage()}aria-label="">First Page</IconButton><IconButtonicon={<ArrowBackIcon />}isDisabled={!getCanPreviousPage()}onClick={() => previousPage()}aria-label="">Prev Page</IconButton><IconButtonicon={<ArrowForwardIcon />}isDisabled={!getCanNextPage()}onClick={() => nextPage()}aria-label="">Next Page</IconButton><IconButtonicon={<ArrowRightIcon />}onClick={() => lastPage()}aria-label="">Last Page</IconButton></HStack></TableContainer>);};
Here is a quick demo of what we have built so far
#Reusing our table
With the current example, we have displayed employee data, but it cannot be extended further to display product data, or order data, or any other data. To make it reusable, we need to decouple the parts specific to the data i.e. the data
and the columns
. We should extract the data fetching logic and the columns in a parent component and pass the data
and columns
as props to our ReactTable
component. This way our ReactTable
can be repurposed for displaying any type of data.
First, remove data and column-related code from our ReactTable
component and create a new EmployeeTable
component that will have context around “Employee” related things, and the ReactTable
component will not even have the context that it is displaying employees.
EmployeeTable.tsx
import { useEffect, useMemo, useState } from "react";import { getTableData } from "../../../api.ts";import { IEmployee } from "../../../utils/types.ts";import { ReactTable } from "./react-table.tsx";import { COLUMNS } from "./columns.ts";export const EmployeeTable = () => {const columns = useMemo(() => COLUMNS, []);const [data, setData] = useState<IEmployee[]>([]);useEffect(() => {const fetchData = async () => {const { datasets } = await getTableData();setData(datasets);};fetchData();}, []);return <ReactTable<IEmployee> data={data} columns={columns} />;};Use Typescript generics to define our `ReactTable` props and destructure those props in the function definition.interface ReactTableProps<T> {data: T[],columns: ColumnDef<T>[]}export const ReactTable = <T,>({ columns, data }: ReactTableProps<T>) => {// ... existing code ...// ... without data & columns ...}
That’s it, now we have our final reusable ReactTable
component, cheers!
import {ArrowBackIcon,ArrowDownIcon,ArrowForwardIcon,ArrowLeftIcon,ArrowRightIcon,ArrowUpIcon,} from "@chakra-ui/icons";import {Heading,HStack,IconButton,Input,Table,TableContainer,Tbody,Td,Th,Thead,Tr,} from "@chakra-ui/react";import {ColumnDef,flexRender,getCoreRowModel,getFilteredRowModel,getPaginationRowModel,getSortedRowModel,SortingState,useReactTable,} from "@tanstack/react-table";import { useState } from "react";interface ReactTableProps<T> {data: T[],columns: ColumnDef<T>[]}export const ReactTable = <T,>({ columns, data }: ReactTableProps<T>) => {const [globalFilter, setGlobalFilter] = useState("");const [sorting, setSorting] = useState<SortingState>([]);const table = useReactTable({columns,data,state: {globalFilter,sorting,},getCoreRowModel: getCoreRowModel(),getFilteredRowModel: getFilteredRowModel(),onGlobalFilterChange: setGlobalFilter,getPaginationRowModel: getPaginationRowModel(),getSortedRowModel: getSortedRowModel(),onSortingChange: setSorting,});const {getHeaderGroups,getRowModel,firstPage,previousPage,lastPage,nextPage,getCanNextPage,getCanPreviousPage,} = table;return (<TableContainer p={8}><Heading>React Table Example</Heading><Inputvalue={globalFilter || ""}onChange={(e) => setGlobalFilter(e.target.value)}placeholder="Search all columns..."my={4}/><Table><Thead>{getHeaderGroups().map((headerGroup) => (<Tr key={headerGroup.id}>{headerGroup.headers.map((header) => (<Thkey={header.id}onClick={header.column.getToggleSortingHandler()}>{header.isPlaceholder? null: flexRender(header.column.columnDef.header,header.getContext())}{header.column.getIsSorted() === "asc" && <ArrowUpIcon />}{header.column.getIsSorted() === "desc" && <ArrowDownIcon />}</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><HStack align="center" justify="center" m={4}><IconButtonicon={<ArrowLeftIcon />}onClick={() => firstPage()}aria-label="">First Page</IconButton><IconButtonicon={<ArrowBackIcon />}isDisabled={!getCanPreviousPage()}onClick={() => previousPage()}aria-label="">Prev Page</IconButton><IconButtonicon={<ArrowForwardIcon />}isDisabled={!getCanNextPage()}onClick={() => nextPage()}aria-label="">Next Page</IconButton><IconButtonicon={<ArrowRightIcon />}onClick={() => lastPage()}aria-label="">Last Page</IconButton></HStack></TableContainer>);};
#Conclusion
In this guide, we have learned how and when to use the React table library in our application. We fetched data on runtime from a HyGraph API and displayed that data in a tabular format using the React Table APIs. We further explored the library and available APIs for utilities like filtering, pagination, and sorting. Finally, we extracted our table component passing it data
and column
as props, so it can be repurposed for various use cases.