Skip to content

Commit

Permalink
docs: column virtualization example (#5245)
Browse files Browse the repository at this point in the history
  • Loading branch information
KevinVandy committed Dec 31, 2023
1 parent 07d71fb commit 43f22c9
Show file tree
Hide file tree
Showing 12 changed files with 414 additions and 1 deletion.
4 changes: 4 additions & 0 deletions docs/config.json
Expand Up @@ -289,6 +289,10 @@
"to": "examples/react/sorting",
"label": "Sorting"
},
{
"to": "examples/react/virtualized-columns",
"label": "Virtualized Columns"
},
{
"to": "examples/react/virtualized-rows",
"label": "Virtualized Rows"
Expand Down
3 changes: 2 additions & 1 deletion docs/guide/virtualization.md
Expand Up @@ -6,6 +6,7 @@ title: Virtualization

Want to skip to the implementation? Check out these examples:

- [virtualized-columns](../examples/react/virtualized-columns)
- [virtualized-rows (dynamic row height)](../examples/react/virtualized-rows)
- [virtualized-rows (fixed row height)](../../../../virtual/v3/docs/examples/react/table)
- [virtualized-infinite-scrolling](../examples/react/virtualized-infinite-scrolling)
Expand All @@ -16,4 +17,4 @@ Want to skip to the implementation? Check out these examples:

## Guide

The TanStack Table packages do not come with any virtualization APIs or features built-in, but TanStack Table can easily work with other virtualization libraries like [react-window](https://www.npmjs.com/package/react-window) or TanStack's own [TanStack Virtual](https://tanstack.com/virtual/v3)
The TanStack Table packages do not come with any virtualization APIs or features built-in, but TanStack Table can easily work with other virtualization libraries like [react-window](https://www.npmjs.com/package/react-window) or TanStack's own [TanStack Virtual](https://tanstack.com/virtual/v3). This guide will show some strategies for using TanStack Table with TanStack Virtual.
5 changes: 5 additions & 0 deletions examples/react/virtualized-columns/.gitignore
@@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
6 changes: 6 additions & 0 deletions examples/react/virtualized-columns/README.md
@@ -0,0 +1,6 @@
# Example

To run this example:

- `npm install` or `yarn`
- `npm run start` or `yarn start`
13 changes: 13 additions & 0 deletions examples/react/virtualized-columns/index.html
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
<script type="module" src="https://cdn.skypack.dev/twind/shim"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
23 changes: 23 additions & 0 deletions examples/react/virtualized-columns/package.json
@@ -0,0 +1,23 @@
{
"name": "tanstack-table-example-virtualized-columns",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview",
"start": "vite"
},
"dependencies": {
"@faker-js/faker": "^8.3.1",
"@tanstack/react-table": "8.11.2",
"@tanstack/react-virtual": "^3.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@rollup/plugin-replace": "^5.0.5",
"@vitejs/plugin-react": "^4.2.1",
"vite": "^5.0.10"
}
}
40 changes: 40 additions & 0 deletions examples/react/virtualized-columns/src/index.css
@@ -0,0 +1,40 @@
html {
font-family: sans-serif;
font-size: 14px;
}

table {
border-collapse: collapse;
border-spacing: 0;
font-family: arial, sans-serif;
table-layout: fixed;
}

thead {
background: lightgray;
}

tr {
border-bottom: 1px solid lightgray;
}

th {
border-bottom: 1px solid lightgray;
border-right: 1px solid lightgray;
padding: 2px 4px;
text-align: left;
}

td {
padding: 6px;
}

.container {
border: 1px solid lightgray;
margin: 1rem auto;
}

.app {
margin: 1rem auto;
text-align: center;
}
238 changes: 238 additions & 0 deletions examples/react/virtualized-columns/src/main.tsx
@@ -0,0 +1,238 @@
import React from 'react'
import ReactDOM from 'react-dom/client'

import './index.css'

import {
ColumnDef,
flexRender,
getCoreRowModel,
getSortedRowModel,
Row,
useReactTable,
} from '@tanstack/react-table'

import { useVirtualizer } from '@tanstack/react-virtual'

import { makeColumns, makeData, Person } from './makeData'

//This is a dynamic row height example, which is more complicated, but allows for a more realistic table.
//See https://tanstack.com/virtual/v3/docs/examples/react/table for a simpler fixed row height example.
function App() {
const columns = React.useMemo<ColumnDef<Person>[]>(
() => makeColumns(1_000),
[]
)

const [data, _setData] = React.useState(() => makeData(1_000, columns))

const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
debugTable: true,
})

const { rows } = table.getRowModel()

//The virtualizers need to know the scrollable container element
const tableContainerRef = React.useRef<HTMLDivElement>(null)

//get first 16 column widths and average them for column size estimation
const averageColumnWidth = React.useMemo(() => {
const columnsWidths =
table
.getRowModel()
.rows[0]?.getCenterVisibleCells()
?.slice(0, 16)
?.map(cell => cell.column.getSize()) ?? []
return columnsWidths.reduce((a, b) => a + b, 0) / columnsWidths.length
}, [table.getRowModel().rows])

//we are using a slightly different virtualization strategy for columns in order to support dynamic row heights
const columnVirtualizer = useVirtualizer({
count: table.getVisibleLeafColumns().length,
estimateSize: () => averageColumnWidth, //average column width in pixels
getScrollElement: () => tableContainerRef.current,
horizontal: true,
overscan: 3,
})

//dynamic row height virtualization - alternatively you could use a simpler fixed row height strategy without the need for `measureElement`
const rowVirtualizer = useVirtualizer({
count: rows.length,
estimateSize: () => 33, //estimate row height for accurate scrollbar dragging
getScrollElement: () => tableContainerRef.current,
//measure dynamic row height, except in firefox because it measures table border height incorrectly
measureElement:
typeof window !== 'undefined' &&
navigator.userAgent.indexOf('Firefox') === -1
? element => element?.getBoundingClientRect().height
: undefined,
overscan: 5,
})

const virtualColumns = columnVirtualizer.getVirtualItems()
const virtualRows = rowVirtualizer.getVirtualItems()

//different virtualization strategy for columns - instead of absolute and translateY, we add empty columns to the left and right
let virtualPaddingLeft: number | undefined
let virtualPaddingRight: number | undefined

if (columnVirtualizer && virtualColumns?.length) {
virtualPaddingLeft = virtualColumns[0]?.start ?? 0
virtualPaddingRight =
columnVirtualizer.getTotalSize() -
(virtualColumns[virtualColumns.length - 1]?.end ?? 0)
}

//All important CSS styles are included as inline styles for this example. This is not recommended for your code.
return (
<div className="app">
{process.env.NODE_ENV === 'development' ? (
<p>
<strong>Notice:</strong> You are currently running React in
development mode. Rendering performance will be slightly degraded
until this application is built for production.
</p>
) : null}
<div>({columns.length.toLocaleString()} columns)</div>
<div>({data.length.toLocaleString()} rows)</div>

<div
className="container"
ref={tableContainerRef}
style={{
overflow: 'auto', //our scrollable table container
position: 'relative', //needed for sticky header
height: '800px', //should be a fixed height
}}
>
{/* Even though we're still using sematic table tags, we must use CSS grid and flexbox for dynamic row heights */}
<table style={{ display: 'grid' }}>
<thead
style={{
display: 'grid',
position: 'sticky',
top: 0,
zIndex: 1,
}}
>
{table.getHeaderGroups().map(headerGroup => (
<tr
key={headerGroup.id}
style={{ display: 'flex', width: '100%' }}
>
{virtualPaddingLeft ? (
//fake empty column to the left for virtualization scroll padding
<th style={{ display: 'flex', width: virtualPaddingLeft }} />
) : null}
{virtualColumns.map(vc => {
const header = headerGroup.headers[vc.index]
return (
<th
key={header.id}
style={{
display: 'flex',
width: header.getSize(),
}}
>
<div
{...{
className: header.column.getCanSort()
? 'cursor-pointer select-none'
: '',
onClick: header.column.getToggleSortingHandler(),
}}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{{
asc: ' 馃敿',
desc: ' 馃斀',
}[header.column.getIsSorted() as string] ?? null}
</div>
</th>
)
})}
{virtualPaddingRight ? (
//fake empty column to the right for virtualization scroll padding
<th style={{ display: 'flex', width: virtualPaddingRight }} />
) : null}
</tr>
))}
</thead>
<tbody
style={{
display: 'grid',
height: `${rowVirtualizer.getTotalSize()}px`, //tells scrollbar how big the table is
position: 'relative', //needed for absolute positioning of rows
}}
>
{virtualRows.map(virtualRow => {
const row = rows[virtualRow.index] as Row<Person>
const visibleCells = row.getVisibleCells()

return (
<tr
data-index={virtualRow.index} //needed for dynamic row height measurement
ref={node => rowVirtualizer.measureElement(node)} //measure dynamic row height
key={row.id}
style={{
display: 'flex',
position: 'absolute',
transform: `translateY(${virtualRow.start}px)`, //this should always be a `style` as it changes on scroll
width: '100%',
}}
>
{virtualPaddingLeft ? (
//fake empty column to the left for virtualization scroll padding
<td
style={{ display: 'flex', width: virtualPaddingLeft }}
/>
) : null}
{virtualColumns.map(vc => {
const cell = visibleCells[vc.index]
return (
<td
key={cell.id}
style={{
display: 'flex',
width: cell.column.getSize(),
}}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</td>
)
})}
{virtualPaddingRight ? (
//fake empty column to the right for virtualization scroll padding
<td
style={{ display: 'flex', width: virtualPaddingRight }}
/>
) : null}
</tr>
)
})}
</tbody>
</table>
</div>
</div>
)
}

const rootElement = document.getElementById('root')

if (!rootElement) throw new Error('Failed to find the root element')

ReactDOM.createRoot(rootElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
18 changes: 18 additions & 0 deletions examples/react/virtualized-columns/src/makeData.ts
@@ -0,0 +1,18 @@
import { faker } from '@faker-js/faker'

export const makeColumns = num =>
[...Array(num)].map((_, i) => {
return {
accessorKey: i.toString(),
header: 'Column ' + i.toString(),
}
})

export const makeData = (num, columns) =>
[...Array(num)].map(() => ({
...Object.fromEntries(
columns.map(col => [col.accessorKey, faker.person.firstName()])
),
}))

export type Person = ReturnType<typeof makeData>[0]
12 changes: 12 additions & 0 deletions examples/react/virtualized-columns/tsconfig.dev.json
@@ -0,0 +1,12 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"composite": true,
"outDir": "./build/types"
},
"files": ["src/main.tsx"],
"include": [
"src"
// "__tests__/**/*.test.*"
]
}

0 comments on commit 43f22c9

Please sign in to comment.