How to Build a Powerful Table in ReactJS with Tanstack and Material UI
Tags: reactjsdevelopment, tanstack, materialui, data
Published: 2023-07-24 21:26:42

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](https://tanstack.com/table/v8) 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](https://tanstack.com/table/v8/docs/guide/introduction)) 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](https://mui.com/).

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](https://gist.github.com/stephengade/db1372466bc6703a8ca53ff75f1e1763).

## 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:

```jsx
// 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 

```jsx
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:

```javascript
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:

```javascript

// 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:

```javascript
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.

```javascript
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:


```javascript
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:

```javascript
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](https://gist.github.com/stephengade/db1372466bc6703a8ca53ff75f1e1763).

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