Logo Sujal Magar
Reusable Table with Shadcn and TanStack Table

Reusable Table with Shadcn and TanStack Table

February 24, 2025
5 min read
Table of Contents

In this blog, you’ll learn how to set up a React app using Vite with Shadcn and TanStack Table for creating a reusable table component. Let’s get started!

1. Create the React App with Vite:

Replace the [project-name] with your actual project name.

npm create vite@latest [project-name] -- --template react-ts --swc
cd [project-name]
npm install

2. Install Shadcn and @TanStack/react-table:

2.1. Shadcn:

2.1.1. Install Tailwind CSS:

npm install tailwindcss @tailwindcss/vite

2.1.2. Configure the Vite plugin:

Install @types/node first:

npm install --save-dev @types/node

Go to vite.config.ts file and set the configuration:

import path from 'path'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
 
// https://vite.dev/config/
export default defineConfig({
  plugins: [react(), tailwindcss()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
})

2.1.3. Configure tsconfig.json file:

{
  "files": [],
  "references": [
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.node.json" }
  ],
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

2.1.4. Configure tsconfig.app.json file:

{
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
 
    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",
 
    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedSideEffectImports": true,
 
    /* Shadcn Config */
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src"]
}

2.1.5. Import Tailwind CSS:

@import 'tailwindcss';

2.1.6. Run the Shadcn CLI:

npx shadcn@latest init

2.1.7. Install Table, Pagination and Buttom component from Shadcn:

npx shadcn@latest add button table pagination

2.2. @TanStack/react-table:

2.2.1 Install @TanStack/react-table:

npm install @tanstack/react-table

3. Create the Table Component:

import * as React from 'react'
import {
  ColumnDef,
  flexRender,
  getCoreRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  SortingState,
  useReactTable,
} from '@tanstack/react-table'
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from './ui/table'
import { Button } from './ui/button'
import { ArrowDown, ArrowUp, ArrowUpDown } from 'lucide-react'
import {
  Pagination,
  PaginationContent,
  PaginationEllipsis,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
} from './ui/pagination'
 
interface PaginationComponentProps {
  currentPage: number
  totalPages: number
  onPageChange: (page: number) => void
}
 
const PaginationComponent = ({
  currentPage,
  totalPages,
  onPageChange,
}: PaginationComponentProps) => {
  return (
    <Pagination>
      <PaginationContent>
        <PaginationItem>
          <PaginationPrevious
            href="#"
            onClick={(e) => {
              e.preventDefault()
              if (currentPage > 1) onPageChange(currentPage - 1)
            }}
          />
        </PaginationItem>
        {[...Array(totalPages)].map((_, index) => {
          const page = index + 1
          if (
            page === 1 ||
            page === totalPages ||
            (page >= currentPage - 1 && page <= currentPage + 1)
          ) {
            return (
              <PaginationItem key={page}>
                <PaginationLink
                  href="#"
                  isActive={page === currentPage}
                  onClick={(e) => {
                    e.preventDefault()
                    onPageChange(page)
                  }}
                >
                  {page}
                </PaginationLink>
              </PaginationItem>
            )
          } else if (page === currentPage - 2 || page === currentPage + 2) {
            return <PaginationEllipsis key={page} />
          }
          return null
        })}
        <PaginationItem>
          <PaginationNext
            href="#"
            onClick={(e) => {
              e.preventDefault()
              if (currentPage < totalPages) onPageChange(currentPage + 1)
            }}
          />
        </PaginationItem>
      </PaginationContent>
    </Pagination>
  )
}
 
interface TableComponentProps<TData, TValue> {
  columns: ColumnDef<TData, TValue>[]
  data: TData[]
  pagination?: {
    page: number
    perPage: number
    totalPages: number
    onPageChange: (page: number) => void
  }
  sorting?: SortingState
  setSorting?: React.Dispatch<React.SetStateAction<SortingState>>
}
 
export const TableComponent = <TData, TValue>({
  columns,
  data,
  pagination,
  sorting,
  setSorting,
}: TableComponentProps<TData, TValue>) => {
  const table = useReactTable({
    data: data,
    columns: columns,
    getCoreRowModel: getCoreRowModel(),
    ...(pagination
      ? {
          getPaginationRowModel: getPaginationRowModel(),
          state: {
            pagination: {
              pageIndex: pagination.page - 1,
              pageSize: pagination.perPage,
            },
          },
          manualPagination: true,
          pageCount: pagination.totalPages,
        }
      : {}),
    onSortingChange: setSorting,
    getSortedRowModel: getSortedRowModel(),
    ...(sorting ? { state: { sorting: sorting } } : {}),
  })
 
  return (
    <div>
      <div className="rounded-md border">
        <Table>
          <TableHeader>
            {table.getHeaderGroups().map((headerGroup) => (
              <TableRow key={headerGroup.id}>
                {headerGroup.headers.map((header) => {
                  const isSortable = header.column.getCanSort()
                  return (
                    <TableHead key={header.id}>
                      {isSortable ? (
                        <Button
                          variant="ghost"
                          onClick={() => header.column.toggleSorting()}
                        >
                          {flexRender(
                            header.column.columnDef.header,
                            header.getContext(),
                          )}
                          {header.column.getIsSorted() === 'asc' ? (
                            <ArrowUp className="ml-2 h-4 w-4" />
                          ) : header.column.getIsSorted() === 'desc' ? (
                            <ArrowDown className="ml-2 h-4 w-4" />
                          ) : (
                            <ArrowUpDown className="ml-2 h-4 w-4" />
                          )}
                        </Button>
                      ) : (
                        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} className="text-left">
                      {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>
      {pagination && (
        <div className="flex items-center justify-end space-x-2 py-4">
          <PaginationComponent
            currentPage={pagination.page}
            totalPages={pagination.totalPages}
            onPageChange={pagination.onPageChange}
          />
        </div>
      )}
    </div>
  )
}

4. Example:

import { useState } from 'react'
import './App.css'
import { TableComponent } from './components/tablecomponent'
import { ColumnDef } from '@tanstack/react-table'
 
interface User {
  id: number
  name: string
  email: string
  role: string
}
 
const columns: ColumnDef<User>[] = [
  {
    accessorKey: 'name',
    header: 'Name',
    cell: (info) => {
      const name = info.row.original.name
      return <div>{name}</div>
    },
    enableSorting: false,
  },
  {
    accessorKey: 'email',
    header: 'Email',
    cell: (info) => {
      const email = info.row.original.email
      return <div>{email}</div>
    },
    enableSorting: false,
  },
  {
    accessorKey: 'role',
    header: 'Role',
    cell: (info) => {
      const role = info.row.original.role
      return <div>{role}</div>
    },
    enableSorting: false,
  },
]
 
const generateUsers = (count: number): User[] => {
  return Array.from({ length: count }, (_, i) => ({
    id: i + 1,
    name: `User ${i + 1}`,
    email: `user${i + 1}@example.com`,
    role: i % 3 === 0 ? 'Admin' : i % 3 === 1 ? 'Editor' : 'Viewer',
  }))
}
 
function App() {
  const [data] = useState<User[]>(() => generateUsers(50))
  const [currentPage, setCurrentPage] = useState(1)
  const [itemsPerPage] = useState(10)
 
  const totalPages = Math.ceil(data.length / itemsPerPage)
 
  const getCurrentPageData = () => {
    const start = (currentPage - 1) * itemsPerPage
    const end = start + itemsPerPage
    return data.slice(start, end)
  }
 
  const handlePageChange = (page: number) => {
    setCurrentPage(page)
  }
 
  return (
    <TableComponent
      columns={columns}
      data={getCurrentPageData()}
      pagination={{
        page: currentPage,
        perPage: itemsPerPage,
        totalPages: totalPages,
        onPageChange: handlePageChange,
      }}
    />
  )
}
 
export default App