Have you ever wondered how some admin panels handle their users in a efficient and comfortable way? Well, tables have come to the rescue!
They're an efficient way to display a huge amount of information with a proper user experience and design. However, before you'd need to work with libraries and dependencies such as Material UI table; and this is a good library, the problem is that it's about 300kb! You're delivering all that Javascript to your client which is not efficient.
Times have changed and now we have alternatives such as Tanstack Table and Shadcn/UI. Amazing UI libraries that allow us to build intuitive and professional tables easily with Typescript support.
That's why in this guide you'll be learning how to build tables using Next.js, a modern fullstack framework based on React, Shadcn/UI, an amazing and modern UI library for React and JSONPlaceholder as our mockup data.
Let's get started!
What We'll be Building
We're going to build a table to display users using Shadcn/UI, Next.js, Tanstack Table and JSONPlaceholder. You'll be able to work with real world data and tables after reading this guide with the mentioned technologies to deliver higher quality products and software.
Project Requirements
To be able to understand and get the most out of this tutorial you need to have Node.js 18.17
or later installed on your computer, a solid understanding of React and Next.js and how to use third party libraries.
Installing Next.js and Shadcn/UI
We're going to install Next.js using the following command from the official website documentation:
npx create-next-app@latest
Use a name of your preference to name your project and since we're going to use Typescript and the App Router, we'll have the following setup:
Would you like to use TypeScript? Yes
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? Yes
Would you like to use src/ directory? Yes
Would you like to use App Router? (recommended) Yes
Would you like to customize the default import alias (@/*) Yes
What import alias would you like configured? @/*
To learn more about the installation process of Next.js, you can check of the official Next.js documentation
Now once the dependencies have been installed go to the project root folder and install Shadcn/UI:
npx shadcn-ui@latest init
You can select the settings you feel the most comfortable with to set up your components.json
file. There will be your Shadcn/UI setup.
Learn more about the installation process of Shadcn/UI using Next.js in the official Shadcn/UI installation documentation with Next.js
Installing Shadcn/UI data table and Tanstack Table
Now that you've installed Shadcn/UI it's time to install the table component from the library. This component can be extended using Tanstack Table so we'll need to install that dependency as well.
Install the table component from Shadcn/UI using the following command:
npx shadcn-ui@latest add table
And install Tanstack Table, which uses React Table under the hood and will allow you to add filtering and pagination in the future:
npm install @tanstack/react-table
See the official TanStack Table Documentation
Now we can start by fetching data and creating the Typescript types.
Getting the data
In a real world application you're going to use actual API endpoints with actual client data. However, in this tutorial we'll be using JSONPlaceholder.
And what is this service? According to the official website:
JSONPlaceholder is a free online REST API that you can use whenever you need some fake data.
In this case, we'll be fetching fictional user data using the following endpoint:
https://jsonplaceholder.typicode.com/users
As you can see, the data we're getting is huge and you might be tempted to create the types yourself. I wouldn't recommend you to do so, but instead, using a website like Transform Tools. This is a website that'll allow us to convert files to other formats such as JSON to Typescript Types or React Props.
Copy the response data you're getting from the API and paste it here: https://transform.tools/json-to-typescript
Create a new file in your src
file called types.ts
and paste the interface from the tool you've used before. You're going to save the types of your project there since this is a small project. Keep in mind you'll need to create and organize different folders for types as your project scales.
Now after some tweaks in the TS interfaces, your data could look like this:
// src/types.ts
export interface User {
id: number
name: string
username: string
email: string
address: Address
phone: string
website: string
company: Company
}
export interface Address {
street: string
suite: string
city: string
zipcode: string
geo: Geo
}
export interface Geo {
lat: string
lng: string
}
export interface Company {
name: string
catchPhrase: string
bs: string
}
Fetching Data and Creating the Services
Let's get started by creating a new folder in our src
folder called services
. In this folder we're going to create our async functions responsible for fetching data.
Create a new file called index.ts
, since we'll only have a single file here for now. Remember to add the return types explicitly as well.
// src/services/index.ts
import { User } from "@/types";
const getUsers = async (): Promise<User[]> => {
const data = await fetch("https://jsonplaceholder.typicode.com/users");
return data.json();
};
export default getUsers;
Columns and How to Use them
Once you've created the services, you can define the columns of your table. Columns are where you define the core of what your table will look like. They define the data that will be displayed, how it will be formatted, sorted and filtered.
Create a new file called columns.tsx
. We're going to display the following fields from the user:
- Username
- Phone Number
- Name
Hence we can create the columns as the following:
"use client"
import { ColumnDef } from "@tanstack/react-table"
import { User } from "@/types"
export const columns: ColumnDef<User>[] = [
{
accessorKey: "username",
header: "Username",
},
{
accessorKey: "email",
header: "Email",
},
{
accessorKey: "phone",
header: "Phone Number",
},
{
accessorKey: "name",
header: "Name"
}
]
What's happening in this code? First, we're creating the column definitions importing the ColumnDef
type definition from Tanstack Table. This is a generic type, so we add the type we want to infer, in this case it's User
from our types.
We create an array with the object definitions we want to display in our table. Each object definition has the properties accessorKey
and header
. accessorKey
refers to the core object field we'll be using, and header
refers to the property's display header in the table.
Learn more about the columns definitions in the official Shadncn/UI documentation
Data Tables and their Purpose
Now it's time to create a reusable <DataTable />
component to render our table.
Go to the folder called ui
inside our components
folder in src
. Create a new file called data-table.tsx
.
You're going to use the following code from the official Shadcn/UI documentation:
// src/components/ui/data-table.tsx
"use client"
import {
ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]
data: TData[]
}
export function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})
return (
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
)
}
Let's see how this code works:
Imports: The component imports necessary functions and components from
@tanstack/react-table
and our table component installed previously@/components/ui/table
.Props: The DataTable component receives two props:
columns
anddata
.columns
represents an array of column definitions, anddata
represents the data to be displayed in the table.useReactTable Hook: Inside the component, the
useReactTable
hook is used to create a table instance. It takesdata
,columns
, andgetCoreRowModel
as parameters.Rendering Header: The component renders the table header by mapping over the header groups obtained from the table instance. For each header group, it maps over the headers and renders a
TableHead
component for each header.Rendering Body: The component renders the table body by mapping over the rows obtained from the table instance. For each row, it renders a
TableRow
component, setting the data-state attribute based on whether the row is selected. Within each row it maps over the visible cells and renders a TableCell component for each cell.FlexRender: The
flexRender
function is used to conditionally render the content of each header and cell based on the provided context.No Results Message: If there are no rows to display, the component renders a single
TableRow
with aTableCell
spanning all columns, displaying a "No results" message.
Displaying the Table in our First Page
Let's use what we've built so far to set up our table in src/app/page.tsx
with the following code:
import { columns } from "@/app/columns";
import { DataTable } from "@/components/ui/data-table";
import getUsers from "@/services";
export default async function Home() {
const data = await getUsers();
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<DataTable columns={columns} data={data} />
</main>
);
}
This code works the following way:
Imports: The code imports the columns definition from the
@/app/columns
file and theDataTable
component from the@/components/ui/data-table
file. Additionally, it imports thegetUsers
function from@/services
.Fetching Data: Inside the
Home
function, thegetUsers
function is called asynchronously to fetch user data. This function makes an HTTP request to an API endpoint to retrieve the data.Rendering: Once the data is fetched, it is passed as props to the
DataTable
component along with the columns definition. TheDataTable
component renders the table UI based on the provided data andcolumn
definitions.Server side rendering: Next.js SSR is used. Server components allow fetching data directly within the component, enabling server-side rendering of data. In this case,
getUsers
is invoked directly within the component to fetch user data.
Final Result
If you've followed the steps properly, your final result should look like this:
Conclusion
Congratulations to reaching this point! You've learned how to create professional tables using Tanstack Table, Next.js and ShadcnUI. You've also learned how to connect to the JSONPlaceholder API and how to consume an API endpoint.
You can also implement pagination, filtering or sorting in the future to keep improving your skills with tables in web development. I'll create more advanced guides in the future about this topic.
Thank you so much for reading this tutorial and I hope you've learned something new!
Top comments (4)
amazing walk through thank you, we look forward to more advanced features like sorting,filtering and searching with tanstack table thank you
It's a pleasure to help my friend! I'll keep those topics in mind to explain them in the future. Thank you so much for supporting my content as well.
I am quite curious to know how you got this to work, when there is stuff missing..........
Oh, my apologies if something isn't working properly. What part of the guide are you having problems with? I'll update the blog post as needed.