Skip to content

Commit ba25124

Browse files
committed
docs: add Excel-like sorting guide for null/undefined handling
Add comprehensive guide for Excel-like sorting behavior Include complete working example with TypeScript Reference community solution from issue #6061 Update API docs to clarify sortUndefined limitations Refs: #6061
1 parent 9c62cf2 commit ba25124

File tree

2 files changed

+203
-0
lines changed

2 files changed

+203
-0
lines changed

docs/api/features/sorting.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ sortUndefined?: 'first' | 'last' | false | -1 | 1 // defaults to 1
139139
140140
> NOTE: `'first'` and `'last'` options are new in v8.16.0
141141
142+
> NOTE: `sortUndefined` only affects undefined values, not null. For handling both null and undefined values (Excel-like sorting), see the [Excel-like Sorting Guide](../guide/excel-like-sorting.md).
143+
142144
## Column API
143145
144146
### `getAutoSortingFn`

docs/guide/excel-like-sorting.md

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
---
2+
title: Excel-like Sorting
3+
---
4+
5+
# Excel-like Sorting with Null/Undefined Values
6+
7+
Excel and other spreadsheet applications handle empty cells in a specific way during sorting - they always appear at the bottom regardless of sort direction. This guide shows how to achieve the same behavior in TanStack Table.
8+
9+
## The Challenge
10+
11+
By default, JavaScript's sorting behavior for null/undefined values can be inconsistent. The `sortUndefined` option only handles undefined values, not null. To achieve true Excel-like sorting, we need a custom approach.
12+
13+
## Solution
14+
15+
### Step 1: Create a Custom Sorting Function
16+
17+
```tsx
18+
const excelLikeSortingFn = (rowA, rowB, columnId) => {
19+
const a = rowA.getValue(columnId);
20+
const b = rowB.getValue(columnId);
21+
22+
// Check for empty values (null, undefined)
23+
const aEmpty = a == null;
24+
const bEmpty = b == null;
25+
26+
// If both are empty, they're equal
27+
if (aEmpty && bEmpty) return 0;
28+
29+
// Empty values always go to bottom
30+
if (aEmpty) return 1;
31+
if (bEmpty) return -1;
32+
33+
// Normal comparison for non-empty values
34+
return a < b ? -1 : a > b ? 1 : 0;
35+
};
36+
```
37+
38+
### Step 2: Apply to Your Columns
39+
40+
```tsx
41+
const columns = [
42+
{
43+
id: 'price',
44+
accessorFn: row => row.price ?? null,
45+
header: 'Price',
46+
cell: ({ getValue }) => {
47+
const value = getValue();
48+
return value == null ? '-' : `$${value}`;
49+
},
50+
sortingFn: excelLikeSortingFn,
51+
sortUndefined: 'last'
52+
}
53+
];
54+
```
55+
56+
### Step 3: Global Configuration (Optional)
57+
58+
Register the sorting function globally for reuse:
59+
60+
```tsx
61+
const table = useReactTable({
62+
data,
63+
columns,
64+
sortingFns: {
65+
excelLike: excelLikeSortingFn
66+
},
67+
defaultColumn: {
68+
sortingFn: 'excelLike'
69+
},
70+
getCoreRowModel: getCoreRowModel(),
71+
getSortedRowModel: getSortedRowModel()
72+
});
73+
```
74+
75+
## Complete Example
76+
77+
```tsx
78+
import React from 'react';
79+
import {
80+
useReactTable,
81+
getCoreRowModel,
82+
getSortedRowModel,
83+
flexRender
84+
} from '@tanstack/react-table';
85+
86+
// Sample data with null/undefined values
87+
const data = [
88+
{ id: 1, product: 'Laptop', price: 999, stock: 10 },
89+
{ id: 2, product: 'Mouse', price: 25, stock: null },
90+
{ id: 3, product: 'Keyboard', price: null, stock: 5 },
91+
{ id: 4, product: 'Monitor', price: 399, stock: undefined },
92+
{ id: 5, product: 'Headphones', price: 89, stock: 0 }
93+
];
94+
95+
function ExcelSortingTable() {
96+
// Excel-like sorting function
97+
const excelLikeSortingFn = (rowA, rowB, columnId) => {
98+
const a = rowA.getValue(columnId);
99+
const b = rowB.getValue(columnId);
100+
101+
if (a == null && b == null) return 0;
102+
if (a == null) return 1;
103+
if (b == null) return -1;
104+
105+
return a < b ? -1 : a > b ? 1 : 0;
106+
};
107+
108+
const columns = React.useMemo(
109+
() => [
110+
{
111+
accessorKey: 'product',
112+
header: 'Product'
113+
},
114+
{
115+
id: 'price',
116+
accessorFn: row => row.price ?? null,
117+
header: 'Price',
118+
cell: ({ getValue }) => {
119+
const value = getValue();
120+
return value == null ? '-' : `$${value}`;
121+
},
122+
sortingFn: excelLikeSortingFn,
123+
sortUndefined: 'last'
124+
},
125+
{
126+
id: 'stock',
127+
accessorFn: row => row.stock ?? null,
128+
header: 'Stock',
129+
cell: ({ getValue }) => {
130+
const value = getValue();
131+
return value == null ? 'N/A' : value;
132+
},
133+
sortingFn: excelLikeSortingFn,
134+
sortUndefined: 'last'
135+
}
136+
],
137+
[]
138+
);
139+
140+
const table = useReactTable({
141+
data,
142+
columns,
143+
getCoreRowModel: getCoreRowModel(),
144+
getSortedRowModel: getSortedRowModel()
145+
});
146+
147+
return (
148+
<table>
149+
<thead>
150+
{table.getHeaderGroups().map(headerGroup => (
151+
<tr key={headerGroup.id}>
152+
{headerGroup.headers.map(header => (
153+
<th
154+
key={header.id}
155+
onClick={header.column.getToggleSortingHandler()}
156+
style={{ cursor: 'pointer' }}
157+
>
158+
{flexRender(
159+
header.column.columnDef.header,
160+
header.getContext()
161+
)}
162+
{{
163+
asc: ' 🔼',
164+
desc: ' 🔽',
165+
}[header.column.getIsSorted()] ?? null}
166+
</th>
167+
))}
168+
</tr>
169+
))}
170+
</thead>
171+
<tbody>
172+
{table.getRowModel().rows.map(row => (
173+
<tr key={row.id}>
174+
{row.getVisibleCells().map(cell => (
175+
<td key={cell.id}>
176+
{flexRender(
177+
cell.column.columnDef.cell,
178+
cell.getContext()
179+
)}
180+
</td>
181+
))}
182+
</tr>
183+
))}
184+
</tbody>
185+
</table>
186+
);
187+
}
188+
189+
export default ExcelSortingTable;
190+
```
191+
192+
## Key Points
193+
194+
- `sortUndefined: 'last'` only handles undefined values, not null
195+
- Custom `sortingFn` is required for consistent null/undefined handling
196+
- `accessorFn` with `?? null` normalizes undefined to null
197+
- `cell` function controls display of empty values
198+
199+
## Credits
200+
201+
This solution was contributed by the community in Issue #6061.

0 commit comments

Comments
 (0)