More complex table
A sortable/filterable table component, wrapped inside a card component. Implemented with modern React and Hyper.
With modern React
Excessive boilerplate through Tanstack Table, ShadCN, and TypeScript interfaces
import * as React from "react"
import {
ColumnDef,
SortingState,
flexRender,
getCoreRowModel,
getSortedRowModel,
getFilteredRowModel,
useReactTable,
ColumnFiltersState,
} from "@tanstack/react-table"
import { ArrowUpDown, Search } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableCaption,
TableRow,
} from "@/components/ui/table"
import {
Card,
CardTitle,
CardHeader,
CardDescription,
CardContent,
} from "@/components/ui/card"
import { Person, DataTableProps } from "./DataTable.types.ts";
// Create a reusable sortable header component
const SortableHeader = ({ column, title, align = "left" }) => (
<div className={align === "right" ? "text-right" : ""}>
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
{title}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
</div>
);
// Define the columns more concisely
const columns: ColumnDef<Person>[] = [
{
accessorKey: "name",
header: ({ column }) => <SortableHeader column={column} title="Name" />,
},
{
accessorKey: "email",
header: ({ column }) => <SortableHeader column={column} title="Email" />,
},
{
accessorKey: "age",
header: ({ column }) => <SortableHeader column={column} title="Age" />,
},
{
accessorKey: "total",
header: ({ column }) => <SortableHeader column={column} title="Total" align="right" />,
cell: ({ row }) => {
const amount = parseFloat(row.getValue("total"))
return <div className="text-right font-medium">{new Intl.NumberFormat('en-US').format(amount)}</div>
},
},
]
export default function DataTable({ data }: DataTableProps) {
const [sorting, setSorting] = React.useState<SortingState>([])
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
const table = useReactTable({
data,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
state: {
sorting,
columnFilters,
},
})
return (
<div className="p-8">
<Card>
<CardHeader>
<CardTitle>Table example</CardTitle>
<CardDescription>A table example with filtering and sortable columns</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center py-4">
<div className="relative max-w-sm">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Filter by name..."
value={(table.getColumn("name")?.getFilterValue() as string) || ""}
onChange={(e) => table.getColumn("name")?.setFilterValue(e.target.value)}
className="pl-8"
/>
</div>
</div>
</CardContent>
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id} className="px-4 py-3">
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="px-7 py-3">
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
<TableCaption className="mb-5">{ data.length } people in total</TableCaption>
</Table>
</Card>
</div>
)
}
Here is the extra TypeScript needed (not included in the comparison images):
type Person = {
id: string;
name: string;
email: string;
age: number;
total: number;
};
interface DataTableProps {
data: Person[]
}
With vanilla TSX
This is an oldschool example using external CSS, which is no longer the idiomatic
way to build React compomnents:
import React, { useState, useMemo } from "react";
import { Person, DataTableProps } from "./DataTable.types.ts";
export default function DataTable({ data }: DataTableProps) {
const [sortField, setSortField] = useState<keyof Person | null>(null);
const [sortDirection, setSortDirection] = useState<1 | -1>(1);
const [filterValue, setFilterValue] = useState("");
const handleSort = (field: keyof Person) => {
if (sortField === field) {
setSortDirection(sortDirection === 1 ? -1 : 1);
} else {
setSortField(field);
setSortDirection(1);
}
};
const filteredData = useMemo(() => {
return filterValue
? data.filter(person =>
person.name.toLowerCase().includes(filterValue.toLowerCase())
)
: data;
}, [data, filterValue]);
const sortedData = useMemo(() => {
if (!sortField) return filteredData;
return [...filteredData].sort((a, b) => {
if (a[sortField] > b[sortField]) return sortDirection;
if (a[sortField] < b[sortField]) return -sortDirection;
return 0;
});
}, [filteredData, sortField, sortDirection]);
return (
<div className="card-container">
<div className="card">
<div className="card-header">
<h1 className="card-title">Table example</h1>
<p className="card-description">A table example with filtering and sortable columns</p>
<div className="search-container">
<input
type="text"
placeholder="Filter by name..."
value={filterValue}
onChange={(e) => setFilterValue(e.target.value)}
className="search-input"
/>
</div>
</div>
<table className="data-table">
<thead>
<tr>
<th>
<button className="sort-button" onClick={() => handleSort("name")}>
Name
<span className="sort-icon">↕</span>
</button>
</th>
<th>
<button className="sort-button" onClick={() => handleSort("email")}>
Email
<span className="sort-icon">↕</span>
</button>
</th>
<th>
<button className="sort-button" onClick={() => handleSort("age")}>
Age
<span className="sort-icon">↕</span>
</button>
</th>
<th>
<button className="sort-button" onClick={() => handleSort("total")}>
Total
<span className="sort-icon">↕</span>
</button>
</th>
</tr>
</thead>
<tbody>
{sortedData.length > 0 ? (
sortedData.map((person) => (
<tr key={person.id}>
<td>{person.name}</td>
<td>{person.email}</td>
<td>{person.age}</td>
<td className="text-right">
{new Intl.NumberFormat('en-US').format(person.total)}
</td>
</tr>
))
) : (
<tr>
<td colSpan={4} className="empty-message">
No results.
</td>
</tr>
)}
</tbody>
<caption className="table-caption">{data.length} people in total</caption>
</table>
</div>
</div>
);
}
With Hyper
Uses only about 40 lines of code, roughly 75% reduction in code to impolement the same features.
<div class="card">
<header>
<h1>Table example</h1>
<p>A table example with filtering and sortable columns</p>
<input type="search" :input="filter" placeholder="Filter by name...">
</header>
<table>
<tr>
<th><a :click="sort('name')">Name</a></th>
<th><a :click="sort('email')">Email</a></th>
<th><a :click="sort('age')">Age</a></th>
<th><a :click="sort('total')">Total</a></th>
</tr>
<tr :for="user of subset || users" key="${ user.id }">
<td>${ user.name }</td>
<td>${ user.email }</td>
<td>${ user.age }</td>
<td>${ new Intl.NumberFormat('en-US').format(user.total) }</td>
</tr>
<tr :if="subset && !subset[0]"><td colspan="4">No results</td></tr>
<caption>${ users.length } people in total</caption>
</table>
<script>
sort(by) {
this.by = this.by == by ? this.by : by
this.dir = this.by == by ? -this.dir || -1 : 1
this.users.sort((a, b) => (a[by] > b[by] ? 1 : -1) * this.dir)
}
filter(e) {
const val = e.target.value.trim().toLowerCase()
this.subset = val ? this.users.filter(el => el.name.toLowerCase().includes(val)) : null
}
</script>
</div>