-
-
Notifications
You must be signed in to change notification settings - Fork 319
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Sticky header in a useReactTable table with useVirtualizer disappears when scrolling #640
Comments
I found a way to fix this, and am linking a working example. The trick is to calculate the height difference between the table element and the scrollable div, and then append an invisible pseudo-element to the table with that calculated height. This results in the table having the same height as the scrollable div, and stops the header from vanishing since you'll no longer scroll past the "end" of table element. Doing this is a little bit tricky since pseudo elements can't be controlled directly through React/JSX inline styling, and directly adding new styles to the DOM is super expensive and forces the browser to recalculate the layout (which will make scrolling janky). The best way I found to do this was with with CSS variables, which the browser is very efficient at handling and it doesn't force a full layout recalc. I added a Also, since the number of table rows ( I'm sure this can be improved and there are further efficiencies, but it works buttery smooth for me. As for the "issue" itself, I would argue that this isn't really a |
@wjthieme i think the example you linked is just the regular virtual table example...there's nothing with sticky headers in it. |
@ryanjoycemacq if you add @riptusk331 I'll check if this works! |
TEMP Workaround - issues see post below
|
There are few issues here, overfall tables don't quite like when rows are positions absolute, one option is to change the positioning strategy by adding before/after row that will move items when scrolling https://codesandbox.io/p/devbox/zealous-firefly-8vm5y2?file=%2Fsrc%2Fmain.tsx%3A91%2C19 btw there is no clear answer what positioning strategy is better, it's really hard to compare performance in a significant way. |
Try applying the transform only if you are not scrolling and add a smooth transition when the header appears
|
@kelvinfloresta |
Here's a clean and working solution: Separate the TableHead and the TableBody and add toggle (or other) buttons as required. Sample implementation with Search Bar and Column Visibility toggles:
|
This doesn't use virtualization. |
Could you possibly add the relevant code to this thread? Your link doesn't work for me |
@wjthieme |
import * as React from 'react'
import { createRoot } from 'react-dom/client'
import { useVirtualizer, notUndefined } from '@tanstack/react-virtual'
import {
ColumnDef,
flexRender,
getCoreRowModel,
getSortedRowModel,
Row,
SortingState,
useReactTable,
} from '@tanstack/react-table'
import { makeData, Person } from './makeData'
import './index.css'
function ReactTableVirtualized() {
const [sorting, setSorting] = React.useState<SortingState>([])
const columns = React.useMemo<ColumnDef<Person>[]>(
() => [
{
accessorKey: 'id',
header: 'ID',
size: 60,
},
{
accessorKey: 'firstName',
cell: (info) => info.getValue(),
},
{
accessorFn: (row) => row.lastName,
id: 'lastName',
cell: (info) => info.getValue(),
header: () => <span>Last Name</span>,
},
{
accessorKey: 'age',
header: () => 'Age',
size: 50,
},
{
accessorKey: 'visits',
header: () => <span>Visits</span>,
size: 50,
},
{
accessorKey: 'status',
header: 'Status',
},
{
accessorKey: 'progress',
header: 'Profile Progress',
size: 80,
},
{
accessorKey: 'createdAt',
header: 'Created At',
cell: (info) => info.getValue<Date>().toLocaleString(),
},
],
[],
)
const [data, setData] = React.useState(() => makeData(50_000))
const table = useReactTable({
data,
columns,
state: {
sorting,
},
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
debugTable: true,
})
const { rows } = table.getRowModel()
const parentRef = React.useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 34,
overscan: 0,
})
const items = virtualizer.getVirtualItems()
const [before, after] =
items.length > 0
? [
notUndefined(items[0]).start - virtualizer.options.scrollMargin,
virtualizer.getTotalSize() - notUndefined(items[items.length - 1]).end,
]
: [0, 0]
const colSpan = 8
return (
<div ref={parentRef} className="container">
<table>
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<th
key={header.id}
colSpan={header.colSpan}
style={{
width: header.getSize(),
position: 'sticky',
top: 0,
backgroundColor: 'red'
}}
>
{header.isPlaceholder ? null : (
<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>
)
})}
</tr>
))}
</thead>
<tbody>
{before > 0 && (
<tr>
<td colSpan={colSpan} style={{ height: before }} />
</tr>
)}
{items.map((virtualRow, index) => {
const row = rows[virtualRow.index] as Row<Person>
return (
<tr
key={row.id}
style={{
height: `${virtualRow.size}px`,
}}
>
{row.getVisibleCells().map((cell) => {
return (
<td key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</td>
)
})}
</tr>
)
})}
{after > 0 && (
<tr>
<td colSpan={colSpan} style={{ height: after }} />
</tr>
)}
</tbody>
</table>
</div>
)
}
function App() {
return (
<div>
<p>
For tables, the basis for the offset of the translate css function is
from the row's initial position itself. Because of this, we need to
calculate the translateY pixel count different and base it off the the
index.
</p>
<ReactTableVirtualized />
<br />
<br />
{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 build for production.
</p>
) : null}
</div>
)
}
const container = document.getElementById('root')
const root = createRoot(container!)
const { StrictMode } = React
root.render(
<StrictMode>
<App />
</StrictMode>,
) |
Here is the working code for your reference
|
Did you solve this problem ? |
here is the snippet that works, i have just copy, pasted did not cleaned it up so please ignore the irrelevant bits.
on tbody this will ensure that your scrollbar indicates true size of table and if you use sticky in header it will not go away.
This will ensure that your virtualized rows are correctly positioned. Last but not the least after this if you have sufficiently large table(million+ rows) you will hit browsers css pixel limit, you can refer to this issue #460 . If its not the case than the current solution should work. with the way current lib is setup, i am not sure if there is a solution with pixel based approach
|
@newbeelearn Thanks for sharing the snippet! You're right, the current implementation doesn't handle the maximum pixel limit, and this is something that can happen with very large tables. Handling this is not trivial, as it introduces additional complexity with managing large offsets. That said, we’re open to ideas and would be happy to accept any PR proposals addressing this. If you have a solution in mind, feel free to contribute! |
@newbeelearn Thanks for sharing your solution ! Sadly, for tables with fixed-layout, setting Is there any solution with relatively positioned table rows ? |
Did you find a solution for this ? @seedydocloop, been stuck on this for a while now |
@seedydocloop @MKSisti relatively positioned rows directly usually won’t work as intended because tables do not behave like regular block elements. |
Thank you for replying 🙏 |
@MKSisti I implemented my solution from this discussion : #585 (comment) Check the codesandbox from @piecyk Sorry, it took me time to find it 🙏 I really think this example should be one mainstream solution shown in the tanstack virtual docs |
@piecyk Do you have any suggestions or solutions for implementing sticky columns when using horizontal virtualization in @tanstack/react-table? |
@piecyk @wjthieme pls check this demo: https://stackblitz.com/edit/tanstack-virtual-wrmyygfq?file=src%2FApp.vue and https://stackblitz.com/edit/tanstack-virtual-nbspetuk?file=src%2FApp.vue |
Describe the bug
When using a react-table together with vritualizer and a sticky header the sticky header disappears when scrolling. This is due to the fact that when combining the two the table should be wrapped in two divs:
Since the table element at any given time only contains the visible rows (plus overscan) the table itself has a height smaller than the wrapper div (nr. 2). This causes the sticky header to disappear when you scroll down since the sticky header cannot escape the table element.
Your minimal, reproducible example
https://codesandbox.io/p/devbox/tanstack-react-virtual-example-dynamic-mr8t3x
Steps to reproduce
position: sticky
to thethead
elementExpected behavior
The header should stay on top since it is sticky.
How often does this bug happen?
Every time
Screenshots or Videos
No response
Platform
Chrome
tanstack-virtual version
3.0.0-beta.68
TypeScript version
5.3.2
Additional context
I've tried getting rid of the wrapper div (nr. 2) and setting the
height:
${getTotalSize()}px` directly on the table element but this causes the rows' height the grow because there are only ever enough rows to fit on the screen (plus overscan) but having the table the full height causes the rows to evenly divide the space between each other (making them a lot larger).Terms & Code of Conduct
The text was updated successfully, but these errors were encountered: