Skip to content

Commit 0a6f4b0

Browse files
committed
chore: polish direct DOM updates PR
1 parent c22de3d commit 0a6f4b0

5 files changed

Lines changed: 38 additions & 35 deletions

File tree

.changeset/direct-dom-updates.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/react-virtual': minor
3+
---
4+
5+
Add opt-in direct DOM updates for scroll positioning with `directDomUpdates`, `directDomUpdatesMode`, and `containerRef`.

examples/react/dynamic/src/main.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ function RowVirtualizerDynamic() {
3838
virtualizer.scrollToIndex(0)
3939
}}
4040
>
41-
scroll to the top2
41+
scroll to the top
4242
</button>
4343
<span style={{ padding: '0 4px' }} />
4444
<button

packages/react-virtual/e2e/app/direct-dom-updates/main.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const App = () => {
99
const parentRef = React.useRef<HTMLDivElement>(null)
1010

1111
const params = new URLSearchParams(window.location.search)
12-
const mode = (params.get('mode') ?? 'position') as 'position' | 'transform'
12+
const mode = (params.get('mode') ?? 'transform') as 'position' | 'transform'
1313

1414
const renderCount = React.useRef(0)
1515
renderCount.current += 1

packages/react-virtual/e2e/app/test/direct-dom-updates.spec.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,13 @@ for (const mode of ['position', 'transform'] as const) {
4747
el.scrollTop = by
4848
}, ITEM_SIZE)
4949

50-
// Give onChange a tick.
51-
await page.waitForTimeout(50)
52-
5350
const item1 = page.locator('[data-testid="item-1"]')
54-
const styleAttr = (await item1.getAttribute('style')) ?? ''
55-
56-
if (mode === 'position') {
57-
expect(styleAttr).toMatch(/top:\s*40px/)
58-
} else {
59-
expect(styleAttr).toMatch(/translate3d\(0px,\s*40px,\s*0px\)/)
60-
}
51+
await expect(item1).toHaveAttribute(
52+
'style',
53+
mode === 'position'
54+
? /top:\s*40px/
55+
: /translate3d\(0px,\s*40px,\s*0px\)/,
56+
)
6157

6258
const renderAfterSmallScroll = Number(
6359
await page.locator('[data-testid="render-count"]').textContent(),

packages/react-virtual/src/index.tsx

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,11 @@ export type ReactVirtualizerOptions<
4040
* or `isScrolling` changes.
4141
*
4242
* Requirements when enabled:
43-
* - Item elements must be `position: absolute` and must NOT set `top` /
44-
* `left` (or `transform: translate*`) in their style — the virtualizer
45-
* owns the main axis.
43+
* - Item elements must be `position: absolute`; in `'transform'` mode they
44+
* must also be anchored with `top: 0` / `left: 0`.
45+
* - Item elements must NOT set the main-axis position in their style — the
46+
* virtualizer owns `top` / `left` in `'position'` mode and `transform` in
47+
* `'transform'` mode.
4648
* - The inner sized container must receive `virtualizer.containerRef` and
4749
* must NOT set `height` / `width` in its style.
4850
* - For multi-lane layouts (grids / masonry), the cross-axis position
@@ -56,12 +58,13 @@ export type ReactVirtualizerOptions<
5658
directDomUpdates?: boolean
5759
/**
5860
* How `directDomUpdates` positions item elements.
59-
* - `'position'` (default): writes `top` / `left`. Item elements must be
61+
* - `'transform'` (default): writes `transform: translate3d(...)`.
62+
* Promotes items to their own compositor layer — usually smoother on long
63+
* lists, but creates a stacking context and can interfere with
64+
* `position: fixed` descendants. Item elements must still be anchored with
65+
* `position: absolute`, `top: 0`, and `left: 0`.
66+
* - `'position'`: writes `top` / `left`. Item elements must be
6067
* `position: absolute`.
61-
* - `'transform'`: writes `transform: translate3d(...)`. Promotes items
62-
* to their own compositor layer — usually smoother on long lists, but
63-
* creates a stacking context and can interfere with `position: fixed`
64-
* descendants.
6568
*/
6669
directDomUpdatesMode?: 'position' | 'transform'
6770
}
@@ -179,21 +182,20 @@ function useVirtualizerBase<
179182
}
180183

181184
const [instance] = React.useState(() => {
182-
const v = new Virtualizer<TScrollElement, TItemElement>(
183-
resolvedOptions,
184-
) as ReactVirtualizer<TScrollElement, TItemElement>
185-
v.containerRef = (node: HTMLElement | null) => {
186-
const state = directRef.current
187-
state.container = node
188-
state.lastSize = null
189-
if (node && state.enabled) {
190-
const total = v.getTotalSize()
191-
state.lastSize = total
192-
const axis = v.options.horizontal ? 'width' : 'height'
193-
node.style[axis] = `${total}px`
194-
}
195-
}
196-
return v
185+
const v = new Virtualizer<TScrollElement, TItemElement>(resolvedOptions)
186+
return Object.assign(v, {
187+
containerRef: (node: HTMLElement | null) => {
188+
const state = directRef.current
189+
state.container = node
190+
state.lastSize = null
191+
if (node && state.enabled) {
192+
const total = v.getTotalSize()
193+
state.lastSize = total
194+
const axis = v.options.horizontal ? 'width' : 'height'
195+
node.style[axis] = `${total}px`
196+
}
197+
},
198+
})
197199
})
198200

199201
instance.setOptions(resolvedOptions)

0 commit comments

Comments
 (0)