Skip to content

Commit aa536e7

Browse files
authored
fix(react-virtual): grow size container before scroll sync on end-anchored prepend (#1237)
1 parent 87f689a commit aa536e7

2 files changed

Lines changed: 34 additions & 5 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/react-virtual': patch
3+
---
4+
5+
Fix a gap at the top of the list after an end-anchored prepend in `directDomUpdates` mode. The prepend grows the total size and bumps `scrollOffset` to the new bottom in the same pass, but the size container's height was written _after_ `_willUpdate` synced the scroll position — so the browser clamped the `scrollTop` write to the stale (shorter) `scrollHeight`, leaving whitespace at the top until the next scroll. The container is now grown before the scroll sync. Only affected `directDomUpdates` mode (React-rendered sizers receive their height during render).

packages/react-virtual/src/index.tsx

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,15 @@ function useVirtualizerBase<
103103
directRef.current.enabled = directDomUpdates
104104
directRef.current.mode = directDomUpdatesMode
105105

106-
// Writes container size + item positions to the DOM. Idempotent — guarded
107-
// by lastSize / lastPositions. Called from onChange (covers scroll-driven
108-
// updates) and from a layout effect (covers post-render commits when refs
109-
// have just registered new items in elementsCache).
110-
const applyDirectStyles = (
106+
// Writes the size container's total extent to the DOM. Idempotent — guarded
107+
// by lastSize. Split out from applyDirectStyles so it can run *before* the
108+
// scroll-position sync in the _willUpdate effect: an end-anchored prepend
109+
// grows the total and bumps scrollOffset in the same pass, and if scrollTop
110+
// is written before the container has grown the browser clamps it to the
111+
// stale (shorter) scrollHeight, leaving a gap at the top until the next
112+
// scroll (visible only in directDomUpdates mode — React-rendered sizers get
113+
// their height during render).
114+
const applyContainerSize = (
111115
instance: Virtualizer<TScrollElement, TItemElement>,
112116
) => {
113117
const state = directRef.current
@@ -119,6 +123,19 @@ function useVirtualizerBase<
119123
const sizeAxis = instance.options.horizontal ? 'width' : 'height'
120124
state.container.style[sizeAxis] = `${totalSize}px`
121125
}
126+
}
127+
128+
// Writes container size + item positions to the DOM. Idempotent — guarded
129+
// by lastSize / lastPositions. Called from onChange (covers scroll-driven
130+
// updates) and from a layout effect (covers post-render commits when refs
131+
// have just registered new items in elementsCache).
132+
const applyDirectStyles = (
133+
instance: Virtualizer<TScrollElement, TItemElement>,
134+
) => {
135+
const state = directRef.current
136+
if (!state.enabled || !state.container) return
137+
138+
applyContainerSize(instance)
122139

123140
const horizontal = !!instance.options.horizontal
124141
const useTransform = state.mode === 'transform'
@@ -205,6 +222,13 @@ function useVirtualizerBase<
205222
}, [])
206223

207224
useIsomorphicLayoutEffect(() => {
225+
// Grow the size container to the new total BEFORE _willUpdate syncs the
226+
// scroll position. On an end-anchored prepend the scroll target lands at
227+
// the new bottom; if the container is still at its old (shorter) height the
228+
// browser clamps the scrollTop write and the list is left with a gap at the
229+
// top until the next scroll. Positions are written afterwards by the
230+
// applyDirectStyles effect below.
231+
applyContainerSize(instance)
208232
return instance._willUpdate()
209233
})
210234

0 commit comments

Comments
 (0)