Skip to content

Commit d45b9a9

Browse files
authoredDec 27, 2023
docs: column sizing/resizing guide (#5228)
1 parent ae4c451 commit d45b9a9

File tree

17 files changed

+911
-238
lines changed

17 files changed

+911
-238
lines changed
 

‎docs/guide/column-sizing.md

+138-2
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,18 @@ title: Column Sizing
77
Want to skip to the implementation? Check out these examples:
88

99
- [column-sizing](../examples/react/column-sizing)
10+
- [column-resizing-performant](../examples/react/column-resizing-performant)
1011

1112
## API
1213

1314
[Column Sizing API](../api/features/column-sizing)
1415

15-
## Overview
16+
## Guide
1617

1718
The column sizing feature allows you to optionally specify the width of each column including min and max widths. It also allows you and your users the ability to dynamically change the width of all columns at will, eg. by dragging the column headers.
1819

20+
### Column Widths
21+
1922
Columns by default are given the following measurement options:
2023

2124
```tsx
@@ -28,9 +31,30 @@ export const defaultColumnSizing = {
2831

2932
These defaults can be overridden by both `tableOptions.defaultColumn` and individual column defs, in that order.
3033

34+
```tsx
35+
const columns = [
36+
{
37+
accessorKey: 'col1',
38+
size: 270, //set column size for this column
39+
},
40+
//...
41+
]
42+
43+
const table = useReactTable({
44+
//override default column sizing
45+
defaultColumn: {
46+
size: 200, //starting column size
47+
minSize: 50, //enforced during column resizing
48+
maxSize: 500, //enforced during column resizing
49+
},
50+
})
51+
```
52+
53+
The column "sizes" are stored in the table state as numbers, and are usually interpreted as pixel unit values, but you can hook up these column sizing values to your css styles however you see fit.
54+
3155
As a headless utility, table logic for column sizing is really only a collection of states that you can apply to your own layouts how you see fit (our example above implements 2 styles of this logic). You can apply these width measurements in a variety of ways:
3256

33-
- `table` elements or any elements being displayed in a table css mode
57+
- semantic `table` elements or any elements being displayed in a table css mode
3458
- `div/span` elements or any elements being displayed in a non-table css mode
3559
- Block level elements with strict widths
3660
- Absolutely positioned elements with strict widths
@@ -39,3 +63,115 @@ As a headless utility, table logic for column sizing is really only a collection
3963
- Really any layout mechanism that can interpolate cell widths into a table structure.
4064

4165
Each of these approaches has its own tradeoffs and limitations which are usually opinions held by a UI/component library or design system, luckily not you 😉.
66+
67+
### Column Resizing
68+
69+
TanStack Table provides built-in column resizing state and APIs that allow you to easily implement column resizing in your table UI with a variety of options for UX and performance.
70+
71+
#### Enable Column Resizing
72+
73+
By default, the `column.getCanResize()` API will return `true` by default for all columns, but you can either disable column resizing for all columns with the `enableColumnResizing` table option, or disable column resizing on a per-column basis with the `enableResizing` column option.
74+
75+
```tsx
76+
const columns = [
77+
{
78+
accessorKey: 'id',
79+
enableResizing: false, //disable resizing for just this column
80+
size: 200, //starting column size
81+
},
82+
//...
83+
]
84+
```
85+
86+
#### Column Resize Mode
87+
88+
By default, the column resize mode is set to `"onEnd"`. This means that the `column.getSize()` API will not return the new column size until the user has finished resizing (dragging) the column. Usually a small UI indicator will be displayed while the user is resizing the column.
89+
90+
In React TanStack Table adapter, where achieving 60 fps column resizing renders can be difficult, depending on the complexity of your table or web page, the `"onEnd"` column resize mode can be a good default option to avoid stuttering or lagging while the user resizes columns. That is not to say that you cannot achieve 60 fps column resizing renders while using TanStack React Table, but you may have to do some extra memoization or other performance optimizations in order to achieve this.
91+
92+
> Advanced column resizing performance tips will be discussed [down below](#advancedcolumnresizingperformance).
93+
94+
If you want to change the column resize mode to `"onChange"` for immediate column resizing renders, you can do so with the `columnResizeMode` table option.
95+
96+
```tsx
97+
const table = useReactTable({
98+
//...
99+
columnResizeMode: 'onChange', //change column resize mode to "onChange"
100+
})
101+
```
102+
103+
#### Column Resize Direction
104+
105+
By default, TanStack Table assumes that the table markup is laid out in a left-to-right direction. For right-to-left layouts, you may need to change the column resize direction to `"rtl"`.
106+
107+
```tsx
108+
const table = useReactTable({
109+
//...
110+
columnResizeDirection: 'rtl', //change column resize direction to "rtl" for certain locales
111+
})
112+
```
113+
114+
#### Connect Column Resizing APIs to UI
115+
116+
There are a few really handy APIs that you can use to hook up your column resizing drag interactions to your UI.
117+
118+
##### Column Size APIs
119+
120+
To apply the size of a column to the column head cells, data cells, or footer cells, you can use the following APIs:
121+
122+
```ts
123+
header.getSize()
124+
column.getSize()
125+
cell.column.getSize()
126+
```
127+
128+
How you apply these size styles to your markup is up to you, but it is pretty common to use either CSS variables or inline styles to apply the column sizes.
129+
130+
```tsx
131+
<th
132+
key={header.id}
133+
colSpan={header.colSpan}
134+
style={{ width: `${header.getSize()}px` }}
135+
>
136+
```
137+
138+
Though, as discussed in the [advanced column resizing performance section](#advancedcolumnresizingperformance), you may want to consider using CSS variables to apply column sizes to your markup.
139+
140+
##### Column Resize APIs
141+
142+
TanStack Table provides a pre-built event handler to make your drag interactions easy to implement. These event handlers are just convenience functions that call other internal APIs to update the column sizing state and re-render the table. Use `header.getResizeHandler()` to connect to your column resize drag interactions, for both mouse and touch events.
143+
144+
```tsx
145+
<ColumnResizeHandle
146+
onMouseDown={header.getResizeHandler()} //for desktop
147+
onTouchStart={header.getResizeHandler()} //for mobile
148+
/>
149+
```
150+
151+
##### Column Resize Indicator with ColumnSizingInfoState
152+
153+
TanStack Table keeps track of an state object called `columnSizingInfo` that you can use to render a column resize indicator UI.
154+
155+
```jsx
156+
<ColumnResizeIndicator
157+
style={{
158+
transform: header.column.getIsResizing()
159+
? `translateX(${table.getState().columnSizingInfo.deltaOffset}px)`
160+
: '',
161+
}}
162+
/>
163+
```
164+
165+
#### Advanced Column Resizing Performance
166+
167+
If you are creating large or complex tables (and using React 😉), you may find that if you do not add proper memoization to your render logic, your users may experience degraded performance while resizing columns.
168+
169+
We have created a [performant column resizing example](../examples/react/column-resizing-performant) that demonstrates how to achieve 60 fps column resizing renders with a complex table that may otherwise have slow renders. It is recommended that you just look at that example to see how it is done, but these are the basic things to keep in mind:
170+
171+
1. Don't use `column.getSize()` on every header and every data cell. Instead, calculate all column widths once upfront, **memoized**!
172+
2. Memoize your Table Body while resizing is in progress.
173+
3. Use CSS variables to communicate column widths to your table cells.
174+
175+
If you follow these steps, you should see significant performance improvements while resizing columns.
176+
177+
If you are not using React, and are using the Svelte, Vue, or Solid adapters instead, you may not need to worry about this as much, but similar principles apply.

‎docs/guide/row-selection.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ const columns = [
161161
]
162162
```
163163

164-
#### Connect Row Selection to Row Click Events
164+
#### Connect Row Selection APIs to UI
165165

166166
If you want a simpler row selection UI, you can just hook up click events to the row itself. The `row.getToggleSelectedHandler()` API is also useful for this use case.
167167

‎docs/guide/virtualization.md

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
title: Virtualization
3+
---
4+
5+
## Examples
6+
7+
Want to skip to the implementation? Check out these examples:
8+
9+
- [virtualized-rows](../examples/react/virtualized-rows)
10+
- [virtualized-infinite-scrolling](../examples/react/virtualized-infinite-scrolling)
11+
12+
## API
13+
14+
[TanStack Virtual Virtualizer API](../../../../virtual/v3/docs/api/virtualizer)
15+
16+
## Guide
17+
18+
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)
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,21 @@
1+
{
2+
"name": "tanstack-table-example-column-resizing-performant",
3+
"version": "0.0.0",
4+
"private": true,
5+
"scripts": {
6+
"dev": "vite",
7+
"build": "vite build",
8+
"serve": "vite preview --port 3001",
9+
"start": "vite"
10+
},
11+
"dependencies": {
12+
"@tanstack/react-table": "8.11.2",
13+
"react": "^18.2.0",
14+
"react-dom": "^18.2.0"
15+
},
16+
"devDependencies": {
17+
"@rollup/plugin-replace": "^5.0.1",
18+
"@vitejs/plugin-react": "^2.2.0",
19+
"vite": "^3.2.3"
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
* {
2+
box-sizing: border-box;
3+
}
4+
5+
html {
6+
font-family: sans-serif;
7+
font-size: 14px;
8+
}
9+
10+
table,
11+
.divTable {
12+
border: 1px solid lightgray;
13+
width: fit-content;
14+
}
15+
16+
.tr {
17+
display: flex;
18+
}
19+
20+
tr,
21+
.tr {
22+
width: fit-content;
23+
height: 30px;
24+
}
25+
26+
th,
27+
.th,
28+
td,
29+
.td {
30+
box-shadow: inset 0 0 0 1px lightgray;
31+
padding: 0.25rem;
32+
}
33+
34+
th,
35+
.th {
36+
padding: 2px 4px;
37+
position: relative;
38+
font-weight: bold;
39+
text-align: center;
40+
height: 30px;
41+
}
42+
43+
td,
44+
.td {
45+
height: 30px;
46+
}
47+
48+
.resizer {
49+
position: absolute;
50+
top: 0;
51+
height: 100%;
52+
right: 0;
53+
width: 5px;
54+
background: rgba(0, 0, 0, 0.5);
55+
cursor: col-resize;
56+
user-select: none;
57+
touch-action: none;
58+
}
59+
60+
.resizer.isResizing {
61+
background: blue;
62+
opacity: 1;
63+
}
64+
65+
@media (hover: hover) {
66+
.resizer {
67+
opacity: 0;
68+
}
69+
70+
*:hover > .resizer {
71+
opacity: 1;
72+
}
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import React from 'react'
2+
import ReactDOM from 'react-dom/client'
3+
4+
import './index.css'
5+
6+
import {
7+
useReactTable,
8+
getCoreRowModel,
9+
ColumnDef,
10+
flexRender,
11+
Table,
12+
} from '@tanstack/react-table'
13+
import { makeData } from './makeData'
14+
15+
type Person = {
16+
firstName: string
17+
lastName: string
18+
age: number
19+
visits: number
20+
status: string
21+
progress: number
22+
}
23+
24+
const defaultColumns: ColumnDef<Person>[] = [
25+
{
26+
header: 'Name',
27+
footer: props => props.column.id,
28+
columns: [
29+
{
30+
accessorKey: 'firstName',
31+
cell: info => info.getValue(),
32+
footer: props => props.column.id,
33+
},
34+
{
35+
accessorFn: row => row.lastName,
36+
id: 'lastName',
37+
cell: info => info.getValue(),
38+
header: () => <span>Last Name</span>,
39+
footer: props => props.column.id,
40+
},
41+
],
42+
},
43+
{
44+
header: 'Info',
45+
footer: props => props.column.id,
46+
columns: [
47+
{
48+
accessorKey: 'age',
49+
header: () => 'Age',
50+
footer: props => props.column.id,
51+
},
52+
{
53+
accessorKey: 'visits',
54+
header: () => <span>Visits</span>,
55+
footer: props => props.column.id,
56+
},
57+
{
58+
accessorKey: 'status',
59+
header: 'Status',
60+
footer: props => props.column.id,
61+
},
62+
{
63+
accessorKey: 'progress',
64+
header: 'Profile Progress',
65+
footer: props => props.column.id,
66+
},
67+
],
68+
},
69+
]
70+
71+
function App() {
72+
const [data, setData] = React.useState(() => makeData(200))
73+
const [columns] = React.useState<typeof defaultColumns>(() => [
74+
...defaultColumns,
75+
])
76+
77+
const rerender = React.useReducer(() => ({}), {})[1]
78+
79+
const table = useReactTable({
80+
data,
81+
columns,
82+
columnResizeMode: 'onChange',
83+
getCoreRowModel: getCoreRowModel(),
84+
debugTable: true,
85+
debugHeaders: true,
86+
debugColumns: true,
87+
})
88+
89+
/**
90+
* Instead of calling `column.getSize()` on every render for every header
91+
* and especially every data cell (very expensive),
92+
* we will calculate all column sizes at once at the root table level in a useMemo
93+
* and pass the column sizes down as CSS variables to the <table> element.
94+
*/
95+
const columnSizeVars = React.useMemo(() => {
96+
const headers = table.getFlatHeaders()
97+
const colSizes: { [key: string]: number } = {}
98+
for (let i = 0; i < headers.length; i++) {
99+
const header = headers[i]!
100+
colSizes[`--header-${header.id}-size`] = header.getSize()
101+
colSizes[`--col-${header.column.id}-size`] = header.column.getSize()
102+
}
103+
return colSizes
104+
}, [table.getState().columnSizingInfo])
105+
106+
//demo purposes
107+
const [enableMemo, setEnableMemo] = React.useState(true)
108+
109+
return (
110+
<div className="p-2">
111+
<i>
112+
This example has artificially slow cell renders to simulate complex
113+
usage
114+
</i>
115+
<div className="h-4" />
116+
<label>
117+
Memoize Table Body:{' '}
118+
<input
119+
type="checkbox"
120+
checked={enableMemo}
121+
onChange={() => setEnableMemo(!enableMemo)}
122+
/>
123+
</label>
124+
<div className="h-4" />
125+
<button onClick={() => rerender()} className="border p-2">
126+
Rerender
127+
</button>
128+
<pre style={{ minHeight: '30rem' }}>
129+
{JSON.stringify(
130+
{
131+
columnSizing: table.getState().columnSizing,
132+
columnSizingInfo: table.getState().columnSizingInfo,
133+
},
134+
null,
135+
2
136+
)}
137+
</pre>
138+
<div className="h-4" />({data.length} rows)
139+
<div className="overflow-x-auto">
140+
{/* Here in the <table> equivalent element (surrounds all table head and data cells), we will define our CSS variables for column sizes */}
141+
<div
142+
{...{
143+
className: 'divTable',
144+
style: {
145+
...columnSizeVars, //Define column sizes on the <table> element
146+
width: table.getTotalSize(),
147+
},
148+
}}
149+
>
150+
<div className="thead">
151+
{table.getHeaderGroups().map(headerGroup => (
152+
<div
153+
{...{
154+
key: headerGroup.id,
155+
className: 'tr',
156+
}}
157+
>
158+
{headerGroup.headers.map(header => (
159+
<div
160+
{...{
161+
key: header.id,
162+
className: 'th',
163+
style: {
164+
width: `calc(var(--header-${header?.id}-size) * 1px)`,
165+
},
166+
}}
167+
>
168+
{header.isPlaceholder
169+
? null
170+
: flexRender(
171+
header.column.columnDef.header,
172+
header.getContext()
173+
)}
174+
<div
175+
{...{
176+
onDoubleClick: () => header.column.resetSize(),
177+
onMouseDown: header.getResizeHandler(),
178+
onTouchStart: header.getResizeHandler(),
179+
className: `resizer ${
180+
header.column.getIsResizing() ? 'isResizing' : ''
181+
}`,
182+
}}
183+
/>
184+
</div>
185+
))}
186+
</div>
187+
))}
188+
</div>
189+
{/* When resizing any column we will render this special memoized version of our table body */}
190+
{table.getState().columnSizingInfo.isResizingColumn && enableMemo ? (
191+
<MemoizedTableBody table={table} />
192+
) : (
193+
<TableBody table={table} />
194+
)}
195+
</div>
196+
</div>
197+
</div>
198+
)
199+
}
200+
201+
//un-memoized normal table body component - see memoized version below
202+
function TableBody({ table }: { table: Table<Person> }) {
203+
return (
204+
<div
205+
{...{
206+
className: 'tbody',
207+
}}
208+
>
209+
{table.getRowModel().rows.map(row => (
210+
<div
211+
{...{
212+
key: row.id,
213+
className: 'tr',
214+
}}
215+
>
216+
{row.getVisibleCells().map(cell => {
217+
//simulate expensive render
218+
for (let i = 0; i < 10000; i++) {
219+
Math.random()
220+
}
221+
222+
return (
223+
<div
224+
{...{
225+
key: cell.id,
226+
className: 'td',
227+
style: {
228+
width: `calc(var(--col-${cell.column.id}-size) * 1px)`,
229+
},
230+
}}
231+
>
232+
{cell.renderValue<any>()}
233+
</div>
234+
)
235+
})}
236+
</div>
237+
))}
238+
</div>
239+
)
240+
}
241+
242+
//special memoized wrapper for our table body that we will use during column resizing
243+
export const MemoizedTableBody = React.memo(
244+
TableBody,
245+
(prev, next) => prev.table.options.data === next.table.options.data
246+
) as typeof TableBody
247+
248+
const rootElement = document.getElementById('root')
249+
if (!rootElement) throw new Error('Failed to find the root element')
250+
251+
ReactDOM.createRoot(rootElement).render(
252+
<React.StrictMode>
253+
<App />
254+
</React.StrictMode>
255+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { faker } from '@faker-js/faker'
2+
3+
export type Person = {
4+
firstName: string
5+
lastName: string
6+
age: number
7+
visits: number
8+
progress: number
9+
status: 'relationship' | 'complicated' | 'single'
10+
subRows?: Person[]
11+
}
12+
13+
const range = (len: number) => {
14+
const arr = []
15+
for (let i = 0; i < len; i++) {
16+
arr.push(i)
17+
}
18+
return arr
19+
}
20+
21+
const newPerson = (): Person => {
22+
return {
23+
firstName: faker.person.firstName(),
24+
lastName: faker.person.lastName(),
25+
age: faker.number.int(40),
26+
visits: faker.number.int(1000),
27+
progress: faker.number.int(100),
28+
status: faker.helpers.shuffle<Person['status']>([
29+
'relationship',
30+
'complicated',
31+
'single',
32+
])[0]!,
33+
}
34+
}
35+
36+
export function makeData(...lens: number[]) {
37+
const makeDataLevel = (depth = 0): Person[] => {
38+
const len = lens[depth]!
39+
return range(len).map((d): Person => {
40+
return {
41+
...newPerson(),
42+
subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
43+
}
44+
})
45+
}
46+
47+
return makeDataLevel()
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"composite": true,
3+
"extends": "../../../tsconfig.base.json",
4+
"compilerOptions": {
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+
})

‎examples/react/column-sizing/src/index.css

+8-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ td,
4747

4848
.resizer {
4949
position: absolute;
50-
right: 0;
5150
top: 0;
5251
height: 100%;
5352
width: 5px;
@@ -57,6 +56,14 @@ td,
5756
touch-action: none;
5857
}
5958

59+
.resizer.ltr {
60+
right: 0;
61+
}
62+
63+
.resizer.rtl {
64+
left: 0;
65+
}
66+
6067
.resizer.isResizing {
6168
background: blue;
6269
opacity: 1;

‎examples/react/column-sizing/src/main.tsx

+283-233
Large diffs are not rendered by default.

‎package.json

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"./examples/react/column-groups",
2727
"./examples/react/column-ordering",
2828
"./examples/react/column-pinning",
29+
"./examples/react/column-resizing-performant",
2930
"./examples/react/column-sizing",
3031
"./examples/react/column-visibility",
3132
"./examples/react/editable-data",

‎packages/table-core/src/features/ColumnSizing.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export interface ColumnSizingOptions {
4040
enableColumnResizing?: boolean
4141
/**
4242
* Enables or disables right-to-left support for resizing the column. defaults to 'ltr'.
43-
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-sizing#rtl)
43+
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-sizing#columnResizeDirection)
4444
* @link [Guide](https://tanstack.com/table/v8/docs/guide/column-sizing)
4545
*/
4646
columnResizeDirection?: ColumnResizeDirection

‎tsconfig.json

+3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
{
2020
"path": "examples/react/column-pinning/tsconfig.dev.json"
2121
},
22+
{
23+
"path": "examples/react/column-resizing-performant/tsconfig.dev.json"
24+
},
2225
{
2326
"path": "examples/react/column-sizing/tsconfig.dev.json"
2427
},

0 commit comments

Comments
 (0)
Please sign in to comment.