Tables
Sortable markets tables and virtualized lists with TanStack Table and onchain-ui cells.
Use AssetRow for a portfolio or watchlist. When users need sortable columns, use a table. This recipe uses TanStack Table for sorting and onchain-ui components to render token data in its cells.
Sortable markets table
The asset column uses TokenLogo, the price column uses TokenPrice, and shadcn's Table provides the markup.
npm install @tanstack/react-tablenpx shadcn add table @onchain-ui/token-logo @onchain-ui/token-price"use client"
import { useState } from "react"
import {
createColumnHelper,
flexRender,
getCoreRowModel,
getSortedRowModel,
useReactTable,
type SortingState,
} from "@tanstack/react-table"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { TokenLogo } from "@/components/ui/token-logo"
import { TokenPrice } from "@/components/ui/token-price"
type Market = {
symbol: string
name: string
src: string | null
price: number
change24h: number
volume24h: number
}
const columnHelper = createColumnHelper<Market>()
const columns = [
columnHelper.accessor("symbol", {
header: "Asset",
cell: ({ row }) => (
<div className="flex items-center gap-2">
<TokenLogo
symbol={row.original.symbol}
name={row.original.name}
src={row.original.src}
size="sm"
/>
<span className="font-medium">{row.original.symbol}</span>
<span className="text-muted-foreground">{row.original.name}</span>
</div>
),
}),
columnHelper.accessor("price", {
header: "Price",
cell: ({ row }) => (
<TokenPrice value={row.original.price} change={row.original.change24h} />
),
}),
columnHelper.accessor("volume24h", {
header: "Volume (24h)",
cell: ({ getValue }) => (
<TokenPrice value={getValue()} compact />
),
}),
]
export function MarketsTable({ markets }: { markets: Market[] }) {
const [sorting, setSorting] = useState<SortingState>([
{ id: "volume24h", desc: true },
])
const table = useReactTable({
data: markets,
columns,
state: { sorting },
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
})
return (
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead
key={header.id}
className="cursor-pointer select-none"
onClick={header.column.getToggleSortingHandler()}
>
{flexRender(header.column.columnDef.header, header.getContext())}
{{ asc: " ↑", desc: " ↓" }[
header.column.getIsSorted() as string
] ?? null}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
)
}Pass your API's market data through the markets prop. See Data fetching for a query with a 60-second polling interval.
Virtual scrolling
If a large list takes too long to render, use TanStack Virtual to mount the visible rows and a small buffer. This example virtualizes a list of AssetRow components:
npm install @tanstack/react-virtualnpx shadcn add @onchain-ui/asset-row"use client"
import { useRef } from "react"
import { useVirtualizer } from "@tanstack/react-virtual"
import { AssetRow, type AssetRowProps } from "@/components/ui/asset-row"
export function VirtualAssetList({ assets }: { assets: AssetRowProps[] }) {
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: assets.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 72, // AssetRow height + gap
overscan: 10,
})
return (
<div ref={parentRef} className="h-96 overflow-y-auto">
<div
className="relative w-full"
style={{ height: virtualizer.getTotalSize() }}
>
{virtualizer.getVirtualItems().map((item) => (
<div
key={item.key}
className="absolute left-0 top-0 w-full pb-2"
style={{ transform: `translateY(${item.start}px)` }}
>
<AssetRow {...assets[item.index]} />
</div>
))}
</div>
</div>
)
}If your virtualized rows contain AddressIdentity, only mounted rows request identity data. That includes the visible rows and the overscan buffer. When a row mounts again, the resolver reuses its cached result.