Describe the bug
VirtualizerController (packages/lit-virtual/src/index.ts) never calls setOptions() after the initial construction, so reactive option updates (a changed count, estimateSize, getScrollElement, etc.) never reach the underlying Virtualizer instance.
constructor(
host: ReactiveControllerHost,
options: VirtualizerOptions<TScrollElement, TItemElement>,
) {
...
this.virtualizer = new Virtualizer(resolvedOptions) // options captured once, here
;(this.host = host).addController(this)
}
hostUpdated() {
this.virtualizer._willUpdate() // no setOptions() call
}
Every other adapter calls setOptions() on every render/update before _willUpdate(). For comparison, react-virtual's useVirtualizer:
instance.setOptions(resolvedOptions)
...
return instance._willUpdate()
and angular-virtual:
virtualizer.setOptions(resolvedOptions())
...
reactiveVirtualizer()._willUpdate()
lit-table's equivalent TableController (same Lit ReactiveController shape, different package) does call the table-core equivalent of setOptions on every access, so this isn't a Lit-specific limitation, lit-virtual just doesn't do it.
Since VirtualizerController is meant to be instantiated once in the host's constructor (the documented usage pattern, same as any other Lit ReactiveController), there's no way for a consumer to get a changed count or other option applied short of reaching into getVirtualizer() and calling .setOptions() manually, which isn't documented anywhere and defeats the point of passing options to the constructor in the first place.
Your minimal, reproducible example
import { LitElement, html } from 'lit'
import { VirtualizerController } from '@tanstack/lit-virtual'
class MyList extends LitElement {
count = 10
virtualizerController = new VirtualizerController(this, {
count: this.count,
estimateSize: () => 35,
getScrollElement: () => this.renderRoot.querySelector('#scroller'),
})
grow() {
this.count = 1000
this.requestUpdate()
}
render() {
const virtualizer = this.virtualizerController.getVirtualizer()
// virtualizer.options.count is still 10 after grow(), even though
// this.count is 1000 and the component re-rendered
return html`<div id="scroller">${virtualizer.getVirtualItems().length} items</div>`
}
}
Steps to reproduce
- Create a
VirtualizerController with an initial count.
- Change the value backing
count and call requestUpdate() on the host.
- Read
virtualizer.options.count (or observe the rendered item count), it still reflects the value from construction time.
Expected behavior
Option changes on subsequent renders should reach the virtualizer, the same as every other framework adapter.
Worth noting this needs more than adding a setOptions() call inside hostUpdated(): the constructor is the only place options is ever received (options: VirtualizerOptions<...>, not a getter), so even a hostUpdated() call would just re-apply the same closed-over object from construction time. The API shape itself doesn't have a channel for fresh options to flow in after construction. lit-table's TableController sidesteps this by exposing a .table(tableOptions, selector) method that's called fresh from render() on every pass rather than once from the constructor, that shape (or an equivalent public setOptions() method consumers call from render()) looks like the fix, not just wiring hostUpdated() to an already-stale value.
Platform
n/a, framework-adapter logic (packages/lit-virtual/src/index.ts)
Describe the bug
VirtualizerController(packages/lit-virtual/src/index.ts) never callssetOptions()after the initial construction, so reactive option updates (a changedcount,estimateSize,getScrollElement, etc.) never reach the underlyingVirtualizerinstance.Every other adapter calls
setOptions()on every render/update before_willUpdate(). For comparison,react-virtual'suseVirtualizer:and
angular-virtual:lit-table's equivalentTableController(same LitReactiveControllershape, different package) does call the table-core equivalent ofsetOptionson every access, so this isn't a Lit-specific limitation,lit-virtualjust doesn't do it.Since
VirtualizerControlleris meant to be instantiated once in the host's constructor (the documented usage pattern, same as any other LitReactiveController), there's no way for a consumer to get a changedcountor other option applied short of reaching intogetVirtualizer()and calling.setOptions()manually, which isn't documented anywhere and defeats the point of passing options to the constructor in the first place.Your minimal, reproducible example
Steps to reproduce
VirtualizerControllerwith an initialcount.countand callrequestUpdate()on the host.virtualizer.options.count(or observe the rendered item count), it still reflects the value from construction time.Expected behavior
Option changes on subsequent renders should reach the virtualizer, the same as every other framework adapter.
Worth noting this needs more than adding a
setOptions()call insidehostUpdated(): the constructor is the only placeoptionsis ever received (options: VirtualizerOptions<...>, not a getter), so even ahostUpdated()call would just re-apply the same closed-over object from construction time. The API shape itself doesn't have a channel for fresh options to flow in after construction.lit-table'sTableControllersidesteps this by exposing a.table(tableOptions, selector)method that's called fresh fromrender()on every pass rather than once from the constructor, that shape (or an equivalent publicsetOptions()method consumers call fromrender()) looks like the fix, not just wiringhostUpdated()to an already-stale value.Platform
n/a, framework-adapter logic (
packages/lit-virtual/src/index.ts)