DEV Community

Cover image for How to Build a Powerful Table in ReactJS with Tanstack and Material UI
Stephen Gbolagade
Stephen Gbolagade

Posted on

How to Build a Powerful Table in ReactJS with Tanstack and Material UI

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";

Enter fullscreen mode Exit fullscreen mode

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

  }
Enter fullscreen mode Exit fullscreen mode

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;
 `;
Enter fullscreen mode Exit fullscreen mode

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);
    };
Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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
]
Enter fullscreen mode Exit fullscreen mode

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


];
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.

⚒️⚒️ I'm currently open for Frontend Engineering and Technical writing role.

Top comments (1)