React, HTML, Headless UI and Tanstack Tables. Part-I
Have you ever found yourself needing a <table /> but are confused with how do “real programmers” implement those fancy table features? Have you broken the production component by shipping that sorting table row code you found on stackoverflow from 10 years ago and you crossed your fingers that it’ll work? You’re on the right blog today, I’ll walk you through tables and cells and the beautifully written Tanstack Table library that will help us write tables without worry.
Introduction
“Writing that feature”. Honestly speaking, I am not that experienced in building applications and I’m still learning everyday from indviduals who are well versed with this world. From writing a few fullstack applications to blogs to hanging out on twitter or twitch. I’ve come to realize that writing from scratch is something I should avoid even if that 3 am coffee thought makes me feel like I’m the “best programmer in the world”. I do wish to write software from scratch and there is nothing wrong with it as I know I’ll learn a lot on why & how something arrived on “the way it is today”. However, when it comes to building things in a team or in optimal way I know I’m expected to maximize both efficiency & time in “writing that feature” so the whole team and product can benefit from my work output.
So should you write your own solution? To answer this, ask yourself this.
The HTML Table Problem
“Do you know how to write a table?” . It’s okay if you don’t, I’ll help you. Thanks to HTML, We have a way to represent tables on web pages. To write a table you would do something like this.
<table>
<thead>
<tr>
<td>Index</td>
<td>Name</td>
<td>Grades</td>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Ayush</td>
<td>F-</td>
</tr>
<tr>
<td>2</td>
<td>Cat</td>
<td>A+</td>
</tr>
<tr>
<td>3</td>
<td>Bob</td>
<td>C</td>
</tr>
</tbody>
</table>
If you save this in a file with a.html
extension. It should give you a table when you open the file with a browser.
Hooray! Pat yourself on the back as we now know how to write a basic table. Okay but, what if we wanted to add more features? What about ordering a table? What about filtering it? What about those fancy column sorting?-
You can quickly see how we run into issues of writing the features from scratch. Well we could, but should we? Is it the optimal way? Do we have time to do so? Is our team large enough?…(so on)
This problem inspired the word “headless”. To understand it, ask yourself the following:
- Is it possible to extract the logic from an existing application that solves a particular problem into independent unit?
The good news is we can. In fact we do this all the time with npm packages/dependencies/libraries. In my own view, this is exactly what “headless” is. A few examples include, Tanstack Table, Tanstack Query, Tanstack Charts, Radix UI, React-Albus, etc.
If you want to dive more into headless and also the story behind tanstack-table
, I recommend checking out the talk from Tanner, where he narrates the tale behind Tanstack Table. It is available here on Youtube.
The Solution
Tanstack Table is available for most major frameworks out there but we’ll be working with React and Nextjs here. The concepts and ideas are all similar and you can translate them to your framework. I’ll try my best to break down the library for you to understand.
The Setup
Part I: Initialization
The most simple way to get started is by using T3 stack. Thanks to nexxel, theo and the community for building such an awesome utility that allows us to quickly bootstrap an application by wrapping all the setup complexity behind some really simple questions.
To get started, Create an empty folder and open up terminal. cd
into your new folder and enter the following:
npm create t3-app@latest
This will guide you through. It’ll look something like this after you answer all the questions. Make sure you select to use typescript & click yes to init a github repo. I’ve also chosen prisma and tRPC here. In short, Prisma is an ORM tool. It’ll manage everything related to the database. tRPC is an awesome project that let’s you quickly scaffold an API without having to setup a dedicated backend/server. It also provides End-To-End typesafety so we can get awesome auto-complete in our project. We might not need Prisma or tRPC for this blog, but it’s cool to have them in case you want to experiment later.
Once you’ve selected the three things. It’ll look something like above. Hit Enter! And let T3 do it’s magic. Once complete, You can cd
into the new project’s directory and we can now install the libraries and get our next app running.
I’ll use yarn
here but you can use both npm
or pnpm
to you liking. Let’s install our libraries by running yarn install
. Once, it’s done installing all the libraries we can now open up the project in our text editor. I’ll also use VSCode here but once again you can use your favorite editor (even notepad). To open up the project in VSCode. Type "code .”
This will open up VSCode with the current directory i.e. our project directory.
We now need to run our application, Simply run yarn dev
to get our application running on localhost. You can do this on terminal in VSCode or use the same terminal from before.
Everything should work! If not comment down below and I’ll try you. You can also use Google. It’s your best friend. Also Congratulations!!! 🎉 We have our app running on localhost. It is already setup with Tailwind, tRPC, Prisma and NextJS out of the box. This is the beauty of T3.
At this point, you should have both a project running on localhost:3000 (by default) and some understanding of the idea of what headless UI is. If your answer is Yes! Lets’ head further. If your answer is “Uhm” or “No” . You can always go back to any section in this blogpost or even go search the internet to your heart’s content.
Part II: Our First HTML Only Table
First we need to get rid of the T3 Boilerplate. Inside of VSCode head over to ./pages/index.tsx
and delete everything inside the return (...)
statement. Once empty. Now paste the below code and don’t worry I’ll walk you through it.
return (
<main className="flex min-h-screen items-center justify-center p-5">
<h1 className="text-3xl text-blue-900">Hey! You're appreciated.</h1>
</main>
);
Here, All I’m doing is returing a <main>
tag that signifies the main content for this application. It’s not necessary you can also return a div. I’m doing this because it’s considered a good practice to have a more recognizable markup. Then, I gave this tag a minimum height of screen and used some tailwind magic to center the content. It also contains a <h1>
tag that has some text in it.
This should render something like this on your screen.
If it does, You’re a great learner. I hope you’re following along with me.
Remember, the old table from before in “The Problem”? It’s his time. Let’s scroll up and copy everything from there. We’ll now render the HTML only table. Let’s see what happens.
I went ahead and removed the <h1>
tag and simply copy-pasted the table code from above in “The Problem” section. You’ll see something like this below after you paste the code.
Whoa! We have our first table. I’ll help you now disect our <table>
markup.
<table>
<thead>
<tr>
<td>Index</td>
<td>Name</td>
<td>Grades</td>
</tr>
</thead>
{/** ... (continued below). Ignore this, this is a comment.*/}
<table>...
. It marks the starting of the table.<thead></thead>
. It holds the table header.<tr></tr>
. It holds the table row.<td></td>
. It holds the cell.
Could you guess what the table markup does now? Pause & Take your time to understand it at your own pace.
Ah, You’re right. The header, holds a single row. A row holds three cells. Each cell contains a word that is the header to our columns.
It’s that simple. Let’s also go through the rest of the table.
{/** ... (continued above). Ignore this this is a comment.*/}
<tbody>
<tr>
<td>1</td>
<td>Ayush</td>
<td>F-</td>
</tr>
<tr>
<td>2</td>
<td>Cat</td>
<td>A+</td>
</tr>
<tr>
<td>3</td>
<td>Bob</td>
<td>C</td>
</tr>
</tbody>
</table>
<tbody> </tbody>
. It holds the body of the table.
The rest of the markup is the same. Here in the body. We have three rows. Each row holds three cells and each cell holds a text value.
At the most basic level, this is the anatomy for defining a table in HTML. If you try to render this, you’ll see.
- One header container <thead> with one row (<tr>) holding three headers in their respective cells (<td>).
- One body container (<tbody>) with three rows (<tr>) again holding three cells (<td>) each containing a value.
If this sounds confusing. Try playing around by changing the values, googling and if you can’t find a answer. Comment down below. I’ll try my best to help you. This blog cannot possibly cover everything in table markup. If you want to learn more. I recommend checking out this blog by FreeCodeCamp. They’ve done an outstanding job at it. Once you have an understanding of tables within HTML. We’ll head over to the next section i.e. Tanstack Tables.
The Tanstack Table (v8)
I wanted to do a long introduction, but you know what. It’s boring. Let’s look at Tanstack table.
In our next/react project. Let’s first install it by typing yarn add @tanstack/react-table
. If you’re on a different framework. This will help you with installing it for your framework. We now need some data. Copy this mock data from the project’s repository in github. Here is the link. You can now paste this inside a data.ts
file in ./utils
directory. I went ahead and already did that.
Now, You’ll notice something very intresting in the same file ( data.ts
) at the very bottom. There’s this line. Do you know what it means?
No worries if you dont. I’ll break this line down for you. I’ve used the magic of typescript to infer the type from our data variable by usingtypeof
keyword provided by typescript. The type for our data is this,
type Person = {
id: number;
name: string;
age: number;
twitter: string;
company: string;
}[];
But there is something odd about it. The type is an array. But our Person is only a single person not an “array of person object”. We need to fix that. So if you remember your high school computer class. You’ll instantly understand why I’ve used[0]
here. All I’m doing is referencing to the first element type which is simply an object and not an array & that’s it. To summerize:
export type People = (typeof data) // an array type, dont add this though, it's here only for you to understand
export type Person = (typeof data)[0] // an object type (inside that array, at 0th index)
Fun bit: Try modifying the first element in the
data
array and hovering over the Person type, you’ll see it get’s updated automatically. Now try modifying the second element in ourdata
array. You’ll see it doesn’t get updated. You need to be careful about this while writing your applications and make sure you don’t get uneven data from an API endpoint. (you can fix this by returning null or empty or a placeholder value. keep your schema consistent.)
Our next step is to get rid of the code we won’t need for our table. In./pages/index.tsx
remove everything inside the<main>
tag in thereturn (...)
statement. The file should look something like this.
import { type NextPage } from "next";
const Home: NextPage = () => {
return (
<main className="flex min-h-screen items-center justify-center p-5"></main>
);
};
export default Home;
We’ll now import the data inside our./pages/index.tsx
so that we can use it for our project. Just to make sure everything works, let’s also console.log()
our data.
import { type NextPage } from "next";
import { data } from "../utils/data"; // importing the data from data.ts file in /utils directory.
const Home: NextPage = () => {
console.log(data); // logging to see if our data actually exists.
return (
<main className="flex min-h-screen items-center justify-center p-5"></main>
);
};
export default Home;
If you open up console (F12 or Right Click → Inspect Element), you’ll see your data is printed, something similar to this.
If you’re wondering, “Why is it being printed twice?”. It is because of React’s Strict Mode. In strict mode, every component is rendered twice. This is a safety measure during development and you can disable it however, it is not recommended unless it affects your development.
Awesome. We have our data now we could use for our table. 🎉
Tanstack Table Library needs two things to render a table. Namely,
- A Column Definition. It is like a schema of our table. It contains information such as, What headers do we have? Which key holds the data? How to render the data? and a lot more information. It’s like the holy bible for our table that a single table instance obeys. We’ll write one pretty soon.
- The Data. It is the data itself. Usually an array of objects fetched from an API endpoint(backend). We already have it in our
/utils/data.ts
file. We’ve also importing it in our./index.ts
file.
That’s all you need to render a table with Tanstack Table.
We’ll now define a Column Definition. To do so. Let’s first analyze our data itself. If you look closely. All we have is an array of objects in our data variable. Let’s look at a single object.
{
id: 0,
name: "Ayush",
age: 18,
twitter: "is_it_ayush",
company: "Doofenshmirtz Evil Inc",
}
Here, id, name, age, twitter, company
are keys that hold our desired values. These keys will be used as “accessorKey” inside our column in Column Definition. The “accessorKey” simply means which key holds the desired value. For a quick example, say we want a column for “Company Name” that display’s the company fromcompany:
key. The accessorKey here will be our company
key. Same goes for others. You’ll understand it more when we write one ourselves and when you’ll see it in action.
From Tanstack Table Documentation,
Column defs are the single most important part of building a table. They are responsible for building the underlying data model that will be used for everything including sorting, filtering, grouping, etc. Formatting the data model into what will be displayed in the table. Creating header groups, headers and footers. Creating columns for display-only purposes, eg. action buttons, checkboxes, expanders, sparklines, etc.
It’s pretty clear but a good analogy would be to compare Column Definitions to a rulebook that the table obeys. It contains answers to questions such as “What columns to display?”, “Which key hold the values that one single column has to display?‘, “How to display the value?”, “What sorting functions to use for a given column?” etc. We’ll now create a Column Definition.
We can do this inside ./index.tsx
file. However, to keep the code clean and human readable. I’ll create another file called ./table.definitions.tsx
in ./utils
directory next to our data.ts
file.
Similarly you can create the file too if you like, otherwise you can also do this inside index.tsx
depending upon what you prefer. Now with that figured out, let’s import a few things we’ll need to write a Column Definition for our table. I’ve explained them in the comments next to the lines so you can copy and paste it in your editor and read it through or even here if you’d prefer.
// We're inside ./utils/table.definitions.tsx file here.
import { Person } from "./data"; // The type of our data.
import { ColumnDef } from "@tanstack/react-table"; // ColumnDef generic from react-table for writing Column Definitions.
That’s it. Now we have everything we need to write our Column Definition. To write one, simply create a variable and give it a type of ColumnDef
. Since ColumnDef
is a generic, we’ll also need to pass in our data type which is Person
. That’s all you need to do. It should look something like this below.
const personDefinition: ColumnDef<Person>[] = []; // we simply pass in our type Person to Column Definition generic.
Now we only need to fill out the personDefinition
array with our columns as objects that we want to display in our table. Like this.
export const personDefinition: ColumnDef<Person>[] = [
{
header: "Id",
accessorKey: "id",
},
{
header: "Name",
accessorKey: "name",
},
{
header: "Age",
accessorKey: "age",
},
{
header: "Company",
accessorKey: "company",
},
{
header: "Twitter",
accessorKey: "twitter",
},
];
In Tanstack Table’s Column Definitions. All our columns are nothing but objects that have a header
property that simply gives them a header name (you can omit this if you don’t want the column to have a header) and an accessorKey
property. You’ll notice that our accessorKey’s are just the key’s from our Person
type that hold the respective value.
For example, the Age column has a header named Age and the key that contains age value in our Person
object is age
. It’s that easy to create a ColumnDefinition.
You can also write accessorFn
that are functions in case you want to render a react component as a cell such as action button’s or anything else.
That’s it. We’ve created our first Column Definition. Pat yourself on the back as you’ve learned something that could be quite complicated especially if you’re new.
But-but- Where is the table? Ah, well Tanstack Table is an Headless UI or to put simply an API. It leaves all the responsibilites of styling and rendering to you. So now we need to render our table & yes we need to use <table>
from HTML markup. Let’s go to our /pages/index.tsx
file and import a few things,
// We're now in /pages/index.tsx file.
import { type NextPage } from "next";
import { data } from "../utils/data";
import { personDefinition } from '../utils/table.definitions'; // Our Column Definition. (make sure to use export to export it in table.definitions.tsx file)
import { useReactTable, flexRender, getCoreRowModel } from '@tanstack/react-table'; // A few important Tanstack Table helpers, that'll help us render our table.
const Home: NextPage = () => {
return (
<main className="flex min-h-screen items-center justify-center p-5"></main>
);
};
export default Home;
We now need to create a table instance. To do that, simply create a variable and use useReactTable()
and pass in some required properties. The only required properties are data, column definition and our getCoreRowModel()
. It should look something like this after you’ve defined it.
const tableInstance = useReactTable({
data,
columns: personDefinition,
getCoreRowModel: getCoreRowModel(),
}); // our tanstack table instance
Let’s quickly see our entire file index.tsx
to make sure we’re on the same page.
import { type NextPage } from 'next';
import { data } from '../utils/data';
import { personDefinition } from '../utils/table.definitions';
import { useReactTable, flexRender, getCoreRowModel } from '@tanstack/react-table';
const Home: NextPage = () => {
const tableInstance = useReactTable({
data,
columns: personDefinition,
getCoreRowModel: getCoreRowModel(),
}); // our tanstack table instance
return (
<main className="flex min-h-screen items-center justify-center p-5">
</main>
);
};
export default Home;
Awesome! Let’s now render our table with the help of our tableInstance
. To do that, we’ll need our <table>
markup. Inside the <main>
tag. Let’s write some code.
<main className="flex min-h-screen items-center justify-center p-5">
<table>
<thead></thead>
<tbody></tbody>
</table>
</main>
Now Inside <thead>
we’ll render our table headers. We can do this by following:
<thead>
{tableInstance.getHeaderGroups().map((headerGroup) => {
return (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<th colSpan={header.colSpan} key={header.id}>
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</th>
);
})}
</tr>
);
})}
</thead>
Don’t worry. I’ll break it down for you. Here we’re first mapping over our header groups. Remember our Column definiation from before? We didn’t group our columns but we could if we wanted too. However, we still need to map over our groups with the help of tableInstance.getHeaderGroups().map()
function. Since we didn’t define them, all our columns will be inside one single group by default.
Then inside our header groups, we’re returning a <tr>
which is table row. Inside the table row. We need to map over each column header itself. This is done by headerGroup.headers.map()
function. It further returns the <th>
itself.
Inside the <th>
we’re simply checking if our table is not a placeholder and if it’s not, then we’ll render it with the help of flexRender()
utility from Tanstack Table. It’s job is to take in a the header as the first arguement and it’s context as the second to render the desired header value.
Now things, should slowly make sense. We’re rendering the header groups → then the headers inside those groups. This is easy once you try it a couple of times. Now if you save the file & look at your application that’s running on localhost:3000. You should see the headers being rendered correctly.
If not, here’s the whole code so you check if you did something wrong.
import { type NextPage } from 'next';
import { data } from '../utils/data';
import { personDefinition } from '../utils/table.definitions';
import { useReactTable, flexRender, getCoreRowModel } from '@tanstack/react-table';
const Home: NextPage = () => {
const tableInstance = useReactTable({
data,
columns: personDefinition,
getCoreRowModel: getCoreRowModel(),
});
return (
<main className="flex min-h-screen items-center justify-center p-5">
<table>
<thead>
{tableInstance.getHeaderGroups().map((headerGroup) => {
return (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<th colSpan={header.colSpan} key={header.id}>
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</th>
);
})}
</tr>
);
})}
</thead>
<tbody></tbody>
</table>
</main>
);
};
export default Home;
Awesome! Let’s now render our tables too. This one is far easier, Inside your <tbody>
markup. You can render the rows like this:
<tbody>
{tableInstance.getRowModel().rows.map((row) => {
return (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => {
return <td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>;
})}
</tr>
);
})}
</tbody>
Let me break this down too. Here we simply map over the rows with the help of tableInstance.getRowModel().rows.map()
function and return a row. Inside the row, we map over the visible cells using the getVisibleCells()
utility and then return a <td>
which if you remember is a table cell. Now again, we use the flexRender()
utlity to render our row cell. here we pass in our cell and it’s context.
All of this should make sufficient sense to you know. If it doesn’t tell me below down in the comments and I’ll try my best to help you out.
Let’s quickly match our entire code in our index.tsx
file so that we can make sure we’re on the same page.
import { type NextPage } from 'next';
import { data } from '../utils/data';
import { personDefinition } from '../utils/table.definitions';
import { useReactTable, flexRender, getCoreRowModel } from '@tanstack/react-table';
const Home: NextPage = () => {
const tableInstance = useReactTable({
data,
columns: personDefinition,
getCoreRowModel: getCoreRowModel(),
});
return (
<main className="flex min-h-screen items-center justify-center p-5">
<table>
<thead>
{tableInstance.getHeaderGroups().map((headerGroup) => {
return (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<th colSpan={header.colSpan} key={header.id}>
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</th>
);
})}
</tr>
);
})}
</thead>
<tbody>
{tableInstance.getRowModel().rows.map((row) => {
return (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => {
return <td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>;
})}
</tr>
);
})}
</tbody>
</table>
</main>
);
};
export default Home;
If that’s it. Save it and goto your T3 app that’s running on the browser. Reload the page and you’ll now see the amazing table being rendered.
🎉 Congratulations for your first Tanstack Table. You should be proud as it took me a while before I understood it all (about 5 days). You’ve done it in a single blog. I’ll suggest you to experiment with the code as much as you can and try to break it so you’ll encounter errors. Read them, google them and you can learn a whole lot when doing so.
Here is the link to the repository that contains the code for this article. The repository is on my github here.
Conclusion
It’s 5 am and I’m too tired to write this (and a bit hungry too), I’ve been writing this since a last week now. So, I’ll end this blog here but I’ll soon follow up with another blog or Part-II where We’ll together explore the feature API’s offered by Tanstack Table. I’ll try my best to make it simple as much as I can for you to understand it. Okay, so I’ma go make me some noodles now 🍜 and thank you for reading this. I know this was long & I’m sorry but I wanted to cover this from problem --> solution
. To give me a little motivation, you can follow me on my twitter. I post a lot there (views are my own) and yeah, let me know if you got any questions or if you can’t understand something. I read them all. Have a great day and we’ll meet again soon in another part, in another blog. ❤