Skip to content

Commit 43f22c9

Browse files
authoredDec 31, 2023
docs: column virtualization example (#5245)
1 parent 07d71fb commit 43f22c9

File tree

12 files changed

+414
-1
lines changed

12 files changed

+414
-1
lines changed
 

‎docs/config.json

+4
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,10 @@
289289
"to": "examples/react/sorting",
290290
"label": "Sorting"
291291
},
292+
{
293+
"to": "examples/react/virtualized-columns",
294+
"label": "Virtualized Columns"
295+
},
292296
{
293297
"to": "examples/react/virtualized-rows",
294298
"label": "Virtualized Rows"

‎docs/guide/virtualization.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ title: Virtualization
66

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

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

1718
## Guide
1819

19-
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)
20+
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.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
.DS_Store
3+
dist
4+
dist-ssr
5+
*.local
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Example
2+
3+
To run this example:
4+
5+
- `npm install` or `yarn`
6+
- `npm run start` or `yarn start`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Vite App</title>
7+
<script type="module" src="https://cdn.skypack.dev/twind/shim"></script>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/main.tsx"></script>
12+
</body>
13+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "tanstack-table-example-virtualized-columns",
3+
"version": "0.0.0",
4+
"private": true,
5+
"scripts": {
6+
"dev": "vite",
7+
"build": "vite build",
8+
"serve": "vite preview",
9+
"start": "vite"
10+
},
11+
"dependencies": {
12+
"@faker-js/faker": "^8.3.1",
13+
"@tanstack/react-table": "8.11.2",
14+
"@tanstack/react-virtual": "^3.0.1",
15+
"react": "^18.2.0",
16+
"react-dom": "^18.2.0"
17+
},
18+
"devDependencies": {
19+
"@rollup/plugin-replace": "^5.0.5",
20+
"@vitejs/plugin-react": "^4.2.1",
21+
"vite": "^5.0.10"
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
html {
2+
font-family: sans-serif;
3+
font-size: 14px;
4+
}
5+
6+
table {
7+
border-collapse: collapse;
8+
border-spacing: 0;
9+
font-family: arial, sans-serif;
10+
table-layout: fixed;
11+
}
12+
13+
thead {
14+
background: lightgray;
15+
}
16+
17+
tr {
18+
border-bottom: 1px solid lightgray;
19+
}
20+
21+
th {
22+
border-bottom: 1px solid lightgray;
23+
border-right: 1px solid lightgray;
24+
padding: 2px 4px;
25+
text-align: left;
26+
}
27+
28+
td {
29+
padding: 6px;
30+
}
31+
32+
.container {
33+
border: 1px solid lightgray;
34+
margin: 1rem auto;
35+
}
36+
37+
.app {
38+
margin: 1rem auto;
39+
text-align: center;
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
import React from 'react'
2+
import ReactDOM from 'react-dom/client'
3+
4+
import './index.css'
5+
6+
import {
7+
ColumnDef,
8+
flexRender,
9+
getCoreRowModel,
10+
getSortedRowModel,
11+
Row,
12+
useReactTable,
13+
} from '@tanstack/react-table'
14+
15+
import { useVirtualizer } from '@tanstack/react-virtual'
16+
17+
import { makeColumns, makeData, Person } from './makeData'
18+
19+
//This is a dynamic row height example, which is more complicated, but allows for a more realistic table.
20+
//See https://tanstack.com/virtual/v3/docs/examples/react/table for a simpler fixed row height example.
21+
function App() {
22+
const columns = React.useMemo<ColumnDef<Person>[]>(
23+
() => makeColumns(1_000),
24+
[]
25+
)
26+
27+
const [data, _setData] = React.useState(() => makeData(1_000, columns))
28+
29+
const table = useReactTable({
30+
data,
31+
columns,
32+
getCoreRowModel: getCoreRowModel(),
33+
getSortedRowModel: getSortedRowModel(),
34+
debugTable: true,
35+
})
36+
37+
const { rows } = table.getRowModel()
38+
39+
//The virtualizers need to know the scrollable container element
40+
const tableContainerRef = React.useRef<HTMLDivElement>(null)
41+
42+
//get first 16 column widths and average them for column size estimation
43+
const averageColumnWidth = React.useMemo(() => {
44+
const columnsWidths =
45+
table
46+
.getRowModel()
47+
.rows[0]?.getCenterVisibleCells()
48+
?.slice(0, 16)
49+
?.map(cell => cell.column.getSize()) ?? []
50+
return columnsWidths.reduce((a, b) => a + b, 0) / columnsWidths.length
51+
}, [table.getRowModel().rows])
52+
53+
//we are using a slightly different virtualization strategy for columns in order to support dynamic row heights
54+
const columnVirtualizer = useVirtualizer({
55+
count: table.getVisibleLeafColumns().length,
56+
estimateSize: () => averageColumnWidth, //average column width in pixels
57+
getScrollElement: () => tableContainerRef.current,
58+
horizontal: true,
59+
overscan: 3,
60+
})
61+
62+
//dynamic row height virtualization - alternatively you could use a simpler fixed row height strategy without the need for `measureElement`
63+
const rowVirtualizer = useVirtualizer({
64+
count: rows.length,
65+
estimateSize: () => 33, //estimate row height for accurate scrollbar dragging
66+
getScrollElement: () => tableContainerRef.current,
67+
//measure dynamic row height, except in firefox because it measures table border height incorrectly
68+
measureElement:
69+
typeof window !== 'undefined' &&
70+
navigator.userAgent.indexOf('Firefox') === -1
71+
? element => element?.getBoundingClientRect().height
72+
: undefined,
73+
overscan: 5,
74+
})
75+
76+
const virtualColumns = columnVirtualizer.getVirtualItems()
77+
const virtualRows = rowVirtualizer.getVirtualItems()
78+
79+
//different virtualization strategy for columns - instead of absolute and translateY, we add empty columns to the left and right
80+
let virtualPaddingLeft: number | undefined
81+
let virtualPaddingRight: number | undefined
82+
83+
if (columnVirtualizer && virtualColumns?.length) {
84+
virtualPaddingLeft = virtualColumns[0]?.start ?? 0
85+
virtualPaddingRight =
86+
columnVirtualizer.getTotalSize() -
87+
(virtualColumns[virtualColumns.length - 1]?.end ?? 0)
88+
}
89+
90+
//All important CSS styles are included as inline styles for this example. This is not recommended for your code.
91+
return (
92+
<div className="app">
93+
{process.env.NODE_ENV === 'development' ? (
94+
<p>
95+
<strong>Notice:</strong> You are currently running React in
96+
development mode. Rendering performance will be slightly degraded
97+
until this application is built for production.
98+
</p>
99+
) : null}
100+
<div>({columns.length.toLocaleString()} columns)</div>
101+
<div>({data.length.toLocaleString()} rows)</div>
102+
103+
<div
104+
className="container"
105+
ref={tableContainerRef}
106+
style={{
107+
overflow: 'auto', //our scrollable table container
108+
position: 'relative', //needed for sticky header
109+
height: '800px', //should be a fixed height
110+
}}
111+
>
112+
{/* Even though we're still using sematic table tags, we must use CSS grid and flexbox for dynamic row heights */}
113+
<table style={{ display: 'grid' }}>
114+
<thead
115+
style={{
116+
display: 'grid',
117+
position: 'sticky',
118+
top: 0,
119+
zIndex: 1,
120+
}}
121+
>
122+
{table.getHeaderGroups().map(headerGroup => (
123+
<tr
124+
key={headerGroup.id}
125+
style={{ display: 'flex', width: '100%' }}
126+
>
127+
{virtualPaddingLeft ? (
128+
//fake empty column to the left for virtualization scroll padding
129+
<th style={{ display: 'flex', width: virtualPaddingLeft }} />
130+
) : null}
131+
{virtualColumns.map(vc => {
132+
const header = headerGroup.headers[vc.index]
133+
return (
134+
<th
135+
key={header.id}
136+
style={{
137+
display: 'flex',
138+
width: header.getSize(),
139+
}}
140+
>
141+
<div
142+
{...{
143+
className: header.column.getCanSort()
144+
? 'cursor-pointer select-none'
145+
: '',
146+
onClick: header.column.getToggleSortingHandler(),
147+
}}
148+
>
149+
{flexRender(
150+
header.column.columnDef.header,
151+
header.getContext()
152+
)}
153+
{{
154+
asc: ' 🔼',
155+
desc: ' 🔽',
156+
}[header.column.getIsSorted() as string] ?? null}
157+
</div>
158+
</th>
159+
)
160+
})}
161+
{virtualPaddingRight ? (
162+
//fake empty column to the right for virtualization scroll padding
163+
<th style={{ display: 'flex', width: virtualPaddingRight }} />
164+
) : null}
165+
</tr>
166+
))}
167+
</thead>
168+
<tbody
169+
style={{
170+
display: 'grid',
171+
height: `${rowVirtualizer.getTotalSize()}px`, //tells scrollbar how big the table is
172+
position: 'relative', //needed for absolute positioning of rows
173+
}}
174+
>
175+
{virtualRows.map(virtualRow => {
176+
const row = rows[virtualRow.index] as Row<Person>
177+
const visibleCells = row.getVisibleCells()
178+
179+
return (
180+
<tr
181+
data-index={virtualRow.index} //needed for dynamic row height measurement
182+
ref={node => rowVirtualizer.measureElement(node)} //measure dynamic row height
183+
key={row.id}
184+
style={{
185+
display: 'flex',
186+
position: 'absolute',
187+
transform: `translateY(${virtualRow.start}px)`, //this should always be a `style` as it changes on scroll
188+
width: '100%',
189+
}}
190+
>
191+
{virtualPaddingLeft ? (
192+
//fake empty column to the left for virtualization scroll padding
193+
<td
194+
style={{ display: 'flex', width: virtualPaddingLeft }}
195+
/>
196+
) : null}
197+
{virtualColumns.map(vc => {
198+
const cell = visibleCells[vc.index]
199+
return (
200+
<td
201+
key={cell.id}
202+
style={{
203+
display: 'flex',
204+
width: cell.column.getSize(),
205+
}}
206+
>
207+
{flexRender(
208+
cell.column.columnDef.cell,
209+
cell.getContext()
210+
)}
211+
</td>
212+
)
213+
})}
214+
{virtualPaddingRight ? (
215+
//fake empty column to the right for virtualization scroll padding
216+
<td
217+
style={{ display: 'flex', width: virtualPaddingRight }}
218+
/>
219+
) : null}
220+
</tr>
221+
)
222+
})}
223+
</tbody>
224+
</table>
225+
</div>
226+
</div>
227+
)
228+
}
229+
230+
const rootElement = document.getElementById('root')
231+
232+
if (!rootElement) throw new Error('Failed to find the root element')
233+
234+
ReactDOM.createRoot(rootElement).render(
235+
<React.StrictMode>
236+
<App />
237+
</React.StrictMode>
238+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { faker } from '@faker-js/faker'
2+
3+
export const makeColumns = num =>
4+
[...Array(num)].map((_, i) => {
5+
return {
6+
accessorKey: i.toString(),
7+
header: 'Column ' + i.toString(),
8+
}
9+
})
10+
11+
export const makeData = (num, columns) =>
12+
[...Array(num)].map(() => ({
13+
...Object.fromEntries(
14+
columns.map(col => [col.accessorKey, faker.person.firstName()])
15+
),
16+
}))
17+
18+
export type Person = ReturnType<typeof makeData>[0]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"extends": "../../../tsconfig.json",
3+
"compilerOptions": {
4+
"composite": true,
5+
"outDir": "./build/types"
6+
},
7+
"files": ["src/main.tsx"],
8+
"include": [
9+
"src"
10+
// "__tests__/**/*.test.*"
11+
]
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as path from 'path'
2+
import { defineConfig } from 'vite'
3+
import react from '@vitejs/plugin-react'
4+
import rollupReplace from '@rollup/plugin-replace'
5+
6+
// https://vitejs.dev/config/
7+
export default defineConfig({
8+
plugins: [
9+
rollupReplace({
10+
preventAssignment: true,
11+
values: {
12+
__DEV__: JSON.stringify(true),
13+
'process.env.NODE_ENV': JSON.stringify('development'),
14+
},
15+
}),
16+
react(),
17+
],
18+
resolve: process.env.USE_SOURCE
19+
? {
20+
alias: {
21+
'react-table': path.resolve(__dirname, '../../../src/index.ts'),
22+
},
23+
}
24+
: {},
25+
})

‎pnpm-lock.yaml

+28
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
Please sign in to comment.