Table has now become an important component in web design and development because it helps in organizing and displaying data in a structured, cleaner, and easy-to-read format.
In native HTML, we have tags such as <table>
, <tr>
, <td>
, <tbody>
, and many other tags to build a Table.
While building with the native HTML tags is very okay, it can become cumbersome in the long run if you're trying to build a table that handles big or large data.
For such, you may need to handle some important logic such as:
- Pagination
- Sorting
- Search
- and so on.
Again, you can handle all these logics from scratch but it's time-consuming and you may end up not optimizing the table for better performance.
That's where React Tanstack Table comes in.
About React Tanstack Table
Tanstack Table is a Headless UI for building powerful tables and data grids.
With the React Tanstack Table package, you can build a powerful and easy-to-customize table in just a few minutes without worrying about performance as the package is built with simplicity and the best performance in mind.
It is free to use and its documentation (here) can help you get started immediately.
Because React Tanstack Table wants to make it customizable (it is headless, remember), you'll have to write the CSS yourself to suit your taste.
But like I said, we want to build a powerful table in a few minutes, so we will use a design system called Material UI to get some of the components we need instead of writing from scratch.
You can read about Material UI here.
React Tanstack Table and Material UI used together can give us a powerful table.
Okay. It seems I'm overhyping this, let's build something!
What we'll be building:
A table that displays some (dummy) data which:
- We can paginate
- We can search
- We can sort.
- Can Memoize for better performance
- Has skeleton loading for better UX
The link to the final code can be found here.
Step 1: Bootstrap a React app
You can use any framework to get your React project running but I'm a big fan of Nextjs, so that's what I'll be using.
run npx create-next-app@latest
Follow the prompts to install the latest version of Nextjs.
(Note: I'm installing Tailwind and Typescript as part of the installation)
Clean up the project by deleting any unused files.
Step 2: Install React Tanstack Table and Material UI
I'm a fan of yarn
but you can also use npm
or any package manager of your choice.
For Tanstack Table, run npm install @tanstack/react-table
For Material UI, run yarn add @mui/material @emotion/react @emotion/styled
This will install MUI, and styled-component which is needed for some components Material UI.
For the table, we also need to use debounce
from lodash, so we'll install the package as well:
run yarn add lodash
If you're using Typescript, you will have to also install types for lodash (yarn add @types/lodash
).
Now that we have everything installed, let's start putting them together.
Step 3: Create the Table 🤳
Note that we are going to create a reusable TableUI component first, so that you can always use the Table anywhere you need it.
Create a new File, called it TableUI.tsx
or TableUI.jsx
for non-typescript project.
We will be importing a couple of items to build this table, so kindly follow along as I'm explaining why they are needed.
in TableUI.tsx:
// from React
import { ChangeEvent, FC, memo, ReactElement, useMemo, useState } from "react";
// From lodash
import { debounce } from "lodash
// From Tanstack
import {
Cell,
ColumnDef,
flexRender,
getCoreRowModel,
Row,
useReactTable,
} from "@tanstack/react-table";
// from Material UI
import {
Box,
Paper,
Skeleton,
Table as MuiTable,
TableHead,
TableRow,
TableCell,
TableBody,
TextField,
Menu,
MenuItem,
Pagination, styled, TableRow
} from "@mui/material";
That's everything we need to import for now.
If you're using Typescript, you have to define the type or interface for the component. Since I'm using Typescript, here is the interface for the TableUI component
interface TableProps {
data: any[];
columns: ColumnDef<any>[];
isFetching?: boolean;
skeletonCount?: number;
skeletonHeight?: number;
headerComponent?: JSX.Element;
pageCount?: number;
page?: (page: number) => void;
search?: (search: string) => void;
onClickRow?: (cell: Cell<any, unknown>, row: Row<any>) => void;
searchLabel?: string;
EmptyText?: string;
children?: React.ReactNode | React.ReactElement
handleRow?: () => void
}
We also need to handle small CSS issue which we'll use styled-component for.
Can be modified, here are CSS:
export const StyledTableRow = styled(TableRow)`
&:nth-of-type(odd) {
background-color: #f1f1f1;
}
&:last-child td,
&:last-child th {
border: 0;
}
:hover {
background-color: #d9d9d9;
}
`;
export const StyledPagination = styled(Pagination)`
display: flex;
justify-content: center;
margin-top: 1rem;
`;
What we want to do next is self-explanatory but I'll try to explain them.
- Pagination
If you are dealing with a dataset of up to one million, you don't want to display everything on a single page. But maybe 20 per page and when the user clicks next, you display the next 20… we'll be doing this
- Memoization
This is simply caching data so that the application will be faster instead for better performance…
- Skeleton loading
Instead of displaying a blank page while the data is still loading, skeleton loading can improve the user experience of your app.
Here are the logics to handle these stuffs with comments to help you understand:
// Initializing state for pagination
const [paginationPage, setPaginationPage] = useState(1);
// Memoizing or caching our data
const memoizedData = useMemo(() => data, [data]);
const memoizedColumns = useMemo(() => columns, [columns]);
const memoisedHeaderComponent = useMemo(
() => headerComponent,
[headerComponent]
);
// Destructing what we need from useReactTable hook provided by Tanstack
const { getHeaderGroups, getRowModel, getAllColumns } = useReactTable({
data: memoizedData,
columns: memoizedColumns,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
pageCount,
});
// For skeleton loading
const skeletons = Array.from({ length: skeletonCount }, (x, i) => i);
// Get Total number of column
const columnCount = getAllColumns().length;
// If there's no Data found
const noDataFound =
!isFetching && (!memoizedData || memoizedData.length === 0);
// onChange function for search
const handleSearchChange = (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
search && search(e.target.value);
};
// onChange function for pagination
const handlePageChange = (
event: ChangeEvent<unknown>,
currentPage: number
) => {
setPaginationPage(currentPage === 0 ? 1 : currentPage);
page?.(currentPage === 0 ? 1 : currentPage);
};
That's everything we need here.
For our table, we just put everything together with Material UI and we'll have something like this:
import {
Box,
Paper,
Skeleton,
Table as MuiTable,
TableHead,
TableRow,
TableCell,
TableBody,
TextField,
Menu,
MenuItem,
Pagination, styled, TableRow
} from "@mui/material";
import {
Cell,
ColumnDef,
flexRender,
getCoreRowModel,
Row,
useReactTable,
} from "@tanstack/react-table";
import { debounce } from "lodash";
import { ChangeEvent, FC, memo, ReactElement, useMemo, useState } from "react";
// Styles with styled-component
export const StyledTableRow = styled(TableRow)`
&:nth-of-type(odd) {
background-color: #f1f1f1;
}
&:last-child td,
&:last-child th {
border: 0;
}
:hover {
background-color: #d9d9d9;
}
`;
export const StyledPagination = styled(Pagination)`
display: flex;
justify-content: center;
margin-top: 1rem;
`;
// Typescript interface
interface TableProps {
data: any[];
columns: ColumnDef<any>[];
isFetching?: boolean;
skeletonCount?: number;
skeletonHeight?: number;
headerComponent?: JSX.Element;
pageCount?: number;
page?: (page: number) => void;
search?: (search: string) => void;
onClickRow?: (cell: Cell<any, unknown>, row: Row<any>) => void;
searchLabel?: string;
EmptyText?: string;
children?: React.ReactNode | React.ReactElement
handleRow?: () => void
}
// The main table
const TableUI: FC<TableProps> = ({
data,
columns,
isFetching,
skeletonCount = 10,
skeletonHeight = 28,
headerComponent,
pageCount,
search,
onClickRow,
page,
searchLabel = "Search",
EmptyText,
children,
handleRow
}) => {
const [paginationPage, setPaginationPage] = useState(1);
const memoizedData = useMemo(() => data, [data]);
const memoizedColumns = useMemo(() => columns, [columns]);
const memoisedHeaderComponent = useMemo(
() => headerComponent,
[headerComponent]
);
const { getHeaderGroups, getRowModel, getAllColumns } = useReactTable({
data: memoizedData,
columns: memoizedColumns,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
pageCount,
});
const skeletons = Array.from({ length: skeletonCount }, (x, i) => i);
const columnCount = getAllColumns().length;
const noDataFound =
!isFetching && (!memoizedData || memoizedData.length === 0);
const handleSearchChange = (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
search && search(e.target.value);
};
const handlePageChange = (
event: ChangeEvent<unknown>,
currentPage: number
) => {
setPaginationPage(currentPage === 0 ? 1 : currentPage);
page?.(currentPage === 0 ? 1 : currentPage);
};
return (
<Paper elevation={2} style={{ padding: "0 0 1rem 0" }}>
<Box paddingX="1rem">
{memoisedHeaderComponent && <Box>{memoisedHeaderComponent}</Box>}
{search && (
<TextField
onChange={debounce(handleSearchChange, 1000)}
size="small"
label={searchLabel}
margin="normal"
variant="outlined"
fullWidth
/>
)}
</Box>
<Box style={{ overflowX: "auto" }}>
<MuiTable>
{!isFetching && (
<TableHead>
{getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-[#000]">
{headerGroup.headers.map((header) => (
<TableCell key={header.id} className="text-white text-sm font-cambon" >
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableCell>
))}
</TableRow>
))}
</TableHead>
)}
<TableBody>
{!isFetching ? (
getRowModel()?.rows.map((row) => (
<StyledTableRow key={row.id} onClick={handleRow}>
{row.getVisibleCells().map((cell) => (
<TableCell
onClick={() => onClickRow?.(cell, row)}
key={cell.id}
className="text-[#2E353A] text-base font-graphik"
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</StyledTableRow>
)
)
) : (
<>
{skeletons.map((skeleton) => (
<TableRow key={skeleton}>
{Array.from({ length: columnCount }, (x, i) => i).map(
(elm) => (
<TableCell key={elm}>
<Skeleton height={skeletonHeight} />
</TableCell>
)
)}
</TableRow>
))}
</>
)}
</TableBody>
</MuiTable>
</Box>
{noDataFound && (
<Box my={2} textAlign="center">
{EmptyText}
</Box>
)}
{pageCount && page && (
<StyledPagination
count={pageCount}
page={paginationPage}
onChange={handlePageChange}
color="primary"
showFirstButton
showLastButton
/>
)}
</Paper>
);
};
Table.defaultProps = {
EmptyText: "No Data is found"
}
export default memo(TableUI);
You now have a resuable Table component courtesy of React Tanstack and Material UI.
Now let's work with it.
Step 4: Our dummy data
I don't have any data API to use in this tutorial, so I'll be using dummy data (as mentioned earlier).
So feel free to use any data structure you like.
Create a new file, call it dummydata.ts
, and put the dummy data there.
export const DummyData = [
{
_id: "0x11",
name: "Stephen",
age: 30,
job: "Frontend Engineer",
country: "Nigeria",
status: "active"
},
{
_id: "0x12",
name: "Gbolagade",
age: 60,
job: "Technical writer",
country: "Nigeria",
status: "not active"
}
// you can add more
]
Step 5: Define the Table Header structure
Create a new file and called it TableHeader.tsx
and see how we set it up.
We'll be importing two items in this file, ColumnDef
for Typescript, and Chip
component from Material UI to handle status from the data:
import { Chip } from "@mui/material";
import { ColumnDef } from "@tanstack/react-table";
export const Columns: ColumnDef<any, any>[] = [
{
accessorKey: "Name",
header: "Name",
cell: (user: any) => {
return <span>{user.row.original.name}</span>
},
},
{
accessorKey: "Age",
header: "Age",
cell: (user: any) => {
return <span>{user.row.original.age}</span>
},
},
{
accessorKey: "Job",
header: "Job",
cell: (user: any) => {
return <span>{user.row.original.job}</span>
},
},
// You can follow the structure to display other data such as country and status
];
It is self-explanatory but note this:
accessorKey
is like a key for each row, so it much be unique… it can be any string but just be unique.
header
is what will appear at the top of the table
cell
is (which) data you're trying to display in that column and we're getting the value from the original row.
That's everything for the Column file.
Now to the last step:
Step 6: Display your Data
We'll handle small logic here, small, I promise.
Create a new file, call it StaffTable.tsx
and follow suite:
import React, { useEffect, useState } from "react";
import { Box } from "@mui/material";
// Import these components correctly depending on your structure
import TableUI from "@/components/TableUI/TableUI";
import { Columns } from "./Column";
import {DummyData} from "./dummydata"
const StaffTable = () => {
// Initiate your states
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [totalPageCount, setTotalPageCount] = useState(1);
// For pagination, define maximum of data per page
const ITEMS_PER_PAGE = 10;
// useEffect to get the data
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
// If you have an API, do your normal axios or fetch
const response = await DummyData
setItems(response);
setTotalPageCount(ITEMS_PER_PAGE)
setLoading(false);
} catch (error) {
console.error("Error fetching data:", error);
setLoading(false);
}
};
fetchData();
}, [currentPage]);
const handlePageChange = (page: any) => {
setCurrentPage(page);
};
// handle the search here
const handleSearch = (query: string) => {
if (query.trim() === "") {
// Restore the original data when the search query is empty
setItems(items);
} else {
const filteredData = items.filter((item: any) =>
item.name.toLowerCase().includes(query.toLowerCase()) ||
item.age.includes(query) ||
item.job.includes(query)
);
setItems(filteredData || []);
}
};
const DataLength = items.length;
return (
<section className="mt-5">
<h3 className="text-[18px] mb-2 md:text-[24px] text-black">
</h3>
<Box>
<TableUI
data={items}
columns={Columns}
searchLabel="Search by Name or job title"
EmptyText="No staff found!"
isFetching={loading}
pageCount={totalPageCount}
page={handlePageChange}
search={handleSearch}
/>
</Box>
</section>
);
};
export default StaffTable;
And that's all… you can now put this component in your page file.
Conclusion: Building React Table with Tanstack and Material UI
React Tanstack is very powerful than what’s illustrated here, that’s why it is advisable you check out its documentation, you’ll be surprised to see how amazing the package is.
While I used Material UI to handle some components here, you can choose to use any design system of your choice, or better still, write your CSS from scratch.
If you have any questions, feel free to ask and I'll glad to answer.
Don't forget: The link to the final code can be found here.
Top comments (1)