Tanstack Table React - Bulletproof Your Table With TypeScript
Tanstack Table React - Bulletproof Your Table With TypeScript

In this post, you will learn how to use the Tanstack Table library to create a fully typed table inside React, ensuring an extremely high level of reliability with TypeScript.

In the current world, the problem with many libraries is that they are not robust enough. You don't always get every autocomplete feature or validation possible, and often, you only get some general Typescript rules for most important methods.

But that's not the case with Tanstack.

website

Tanstack offers a variety of libraries that are extremely type-safe and represent the pinnacle of how TypeScript should be written. The most popular library here is Tanstack Query, which is used for fetching data from APIs. The second most popular library is Tanstack Table.

Tanstack Table is a Component Development Kit (CDK). It doesn't include any styling out of the box. If you compare it with something like Angular Material, you won't find any pre-defined styles. Instead, it's more similar to the Angular CDK, where you get functions to help you build your table.

So, no styling — just useful functions. However, you'll get enough functions to build a table of any complexity.

Another important point is that it is framework agnostic. You can use it with React, Solid, Svelte, Vue, or vanilla JavaScript and TypeScript.

What you get inside are features like rendering tables, columns, headers, cells, ordering columns, changing visibility and sizing, filtering, grouping, and pagination. It covers a wide range of cases that you might encounter, and all of this is supported extremely well with TypeScript.

That's why in this post, I will show you how to build a user table with sorting and filtering using Tanstack.

Project

Our first step is to install the library @tanstack/react-table because we are building a table for React.

npm i @tanstack/react-table

Now let's look on the project that I prepared.

import UsersTable from "./usersTable/UsersTable";

function App() {
  return (
    <div className="App">
      <UsersTable />
    </div>
  );
}

Inside App, we simply render a UsersTable which contains all the necessary logic.

/* src/usersTable/usersTable.css */
.users-table {
  table-layout: fixed;
  width: 100%;
  border-collapse: collapse;
}

.users-table-cell {
  border: 1px solid #dddddd;
  text-align: left;
  padding: 8px;
}

.search-bar {
  margin-bottom: 20px;
}

Here we have some styling that we will use later.

// src/usersTable/UsersTable.tsx
import { FormEvent, useEffect, useState } from "react";
import "./usersTable.css";

export type User = {
  id: number;
  name: string;
  age: number;
};

const UsersTable = () => {
  const [searchValue, setSearchValue] = useState("");
  const [inputSearchValue, setInputSearchValue] = useState("");
  const submitSearchForm = (e: FormEvent) => {
    e.preventDefault();
    setSearchValue(inputSearchValue);
  };

  return (
    <div>
      <div className="search-bar">
        <form onSubmit={submitSearchForm}>
          <input
            type="text"
            placeholder="Search..."
            value={inputSearchValue}
            onChange={(e) => setInputSearchValue(e.target.value)}
          />
        </form>
      </div>
      <table className="users-table">
        <thead>
        </thead>
        <tbody>
        </tbody>
      </table>
    </div>
  );
};

export default UsersTable;

Here is the table component that I prepared for us. It has an input used for filtering later and an empty table. Additionally, I've defined a type User with id, name, and age because this is exactly what we want to render in our table.

Setting Up Columns

Let's start by implementing Tanstack Table. The first thing we need to implement is columns.

const columnHelper = createColumnHelper<User>();

const columns = [
  columnHelper.accessor("id", {
    header: () => "ID",
    cell: (info) => info.getValue(),
  }),
  columnHelper.accessor("name", {
    header: () => "Name",
    cell: (info) => info.getValue(),
  }),
  columnHelper.accessor("age", {
    header: () => "Age",
    cell: (info) => info.getValue(),
  }),
];
const UsersTable = () => {
  ...
}

Even before our component, we want to define all possible columns for our table. To do that, we use createColumnHelper from Tanstack, specifying User as the data type for the table.

Inside columns, we specify a header for each column and what info is rendered in the cell.

columns autocomplete

As you can see, while typing, we get amazing autocomplete that understands what column names we can use in the table. This is the magic of TypeScript when done at a high level.

Creating a Table

Now we want to use a hook to create a table. But in order to do that, we must define the data which will go to our table.

const UsersTable = () => {
  ...
  const [users, setUsers] = useState<User[]>([]);
  const table = useReactTable({
    data: users,
    columns,
    getCoreRowModel: getCoreRowModel(),
    debugTable: true,
  });
  ...
}

Here we created a users state and a table with useReactTable. Inside, we provide our data, columns, and getCoreRowModel.

Core row model

Since we don't know what coreRowModel is, let's search for it in the official documentation. It is nicely explained, and now we know that for each table, we must use getCoreRowModel to calculate and return the row model.

Rendering Table

We have successfully prepared our table. Now it is time to render it inside our thead and tbody.

<thead>
  {table.getHeaderGroups().map((headerGroup) => (
    <tr key={headerGroup.id}>
      {headerGroup.headers.map((header) => (
        <th key={header.id} className="users-table-cell">
          <div>
            {flexRender(
              header.column.columnDef.header,
              header.getContext()
            )}
          </div>
        </th>
      ))}
    </tr>
  ))}
</thead>

This code might look complex, but it's because this library is well-developed and extremely flexible. Since it's possible to render multiple headers, we start by rendering header groups. Every header group represents a row. We have just a single row in our table. Inside this row, we want to render all headers (header cells) that we have. To render information inside the cell, we use the flexRender function, which can render anything like header cells, table cells, and even footers.

I understand it might be confusing because we call lots of different functions from this library, and you might not know exactly what they are doing. But bear with me because this is what makes this library highly configurable. You can't make a library configurable when everything is simply made.

rendered header

As you can see, we successfully rendered our header with ID, name, and age. Now let's do exactly the same with tbody.

<tbody>
  {table.getRowModel().rows.map((row) => (
    <tr key={row.id}>
      {row.getVisibleCells().map((cell) => (
        <td key={cell.id} className="users-table-cell">
          {flexRender(cell.column.columnDef.cell, cell.getContext())}
        </td>
      ))}
    </tr>
  ))}
</tbody>

Here we render our rows, and inside each, we render visible cells. Since we don't have hidden cells, we just render all our cells here. Inside every single cell, we render its information using flexRender.

We finished rendering our table, but we won't see anything as our users array is empty. Let's add at least one user for testing.

const [users, setUsers] = useState<User[]>([
  {id: '1', name: 'Foo', age: 40}
]);

single user

As you can see, we successfully rendered our table with a single user.

Fetching Data

But this is not what we want. We don't need static data in our application. We want to fetch our users from the API as in a real project. I have a running json-server that allows us to create an API in a matter of seconds based on a db.json file.

{
  "users": [
    {
      "id": 1,
      "name": "Jack",
      "age": 20
    },
    {
      "id": 2,
      "name": "John",
      "age": 25
    },
    {
      "id": 3,
      "name": "Mike",
      "age": 30
    }
  ]
}

We specified 3 users in our API, and we can fetch them now.

useEffect(() => {
  const url = `http://localhost:3004/users`;
  fetch(url)
    .then((res) => res.json())
    .then((users) => {
      setUsers(users);
    });
}, []);

three users

As you can see, we successfully fetched users from the API, and Tanstack updates the table when the data inside our users change.

Filtering Data

But that's not all. We also have a search bar at the top of our table, and it would be nice to filter our data. Essentially, this logic is not related at all to Tanstack table because we just need to apply a filter to our API request.

  useEffect(() => {
    const url = `http://localhost:3004/users?name_like=${searchValue}`;
    fetch(url)
      .then((res) => res.json())
      .then((users) => {
        setUsers(users);
      });
  }, [searchValue]);

Our useEffect is dependent on searchValue. Now, every time our searchValue changes, we make an API call with a filter.

filter

Here we typed "Jack" in the search bar, and our table was filtered because we got new data from the API.

Adding Sorting

What about sorting functionality? It is completely possible to implement it inside Tanstack Table.

const [sorting, setSorting] = useState<SortingState>([]);
const table = useReactTable({
  data: users,
  columns,
  state: {
    sorting,
  },
  onSortingChange: setSorting,
  getCoreRowModel: getCoreRowModel(),
  debugTable: true,
});

First of all, we must create sorting state and provide it inside our useReactTable. We call our setSorting function every time sorting happens inside Tanstack Table. This is all that we need to implement sorting. But we don't have any buttons to change sorting inside our table.

<thead>
  {table.getHeaderGroups().map((headerGroup) => (
    <tr key={headerGroup.id}>
      {headerGroup.headers.map((header) => (
        <th key={header.id} className="users-table-cell">
          <div
            {...{
              className: header.column.getCanSort()
                ? "cursor-pointer select-none"
                : "",
              onClick: header.column.getToggleSortingHandler(),
            }}
          >
            {flexRender(
              header.column.columnDef.header,
              header.getContext()
            )}
            {{
              asc: " 🔼",
              desc: " 🔽",
            }[header.column.getIsSorted() as string] ?? null}
          </div>
        </th>
      ))}
    </tr>
  ))}
</thead>

Here we updated our thead to include sorting buttons. First, we add styles cursor-pointer select-none to our div when we can sort this header. We also added a click event which calls sorting for a specific column. After our flexRender, we added an icon for ascending and descending sorting based on how this column is sorted.

The only thing that we are missing is to apply sorting to our API call.

useEffect(() => {
  const order = sorting[0]?.desc ? "desc" : "asc";
  const sort = sorting[0]?.id ?? "id";
  const url = `http://localhost:3004/users?_sort=${sort}&_order=${order}&name_like=${searchValue}`;
  fetch(url)
    .then((res) => res.json())
    .then((users) => {
      setUsers(users);
    });
}, [sorting, searchValue]);

Our sorting state is an array because Tanstack supports multi-column sorting. Here we read which column we sort and add this information to our API call.

sorting

As you can see, sorting is working as expected inside our project.

So, Tanstack Table is not a simple solution. But it is extremely flexible, and when you get used to it, your application is Bulletproof with TypeScript.

And actually, if you want to improve your React knowledge and prepare for interviews, I highly recommend you check out my course React Interview Questions.

📚 Source code of what we've done