Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/hungry-buses-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/virtual-core': patch
---

revert(virtual-core): "notify framework when count changes" 2542c5a
4 changes: 2 additions & 2 deletions docs/api/virtualizer.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,9 @@ The position where the list is scrolled to on render. This is useful if you are
getItemKey?: (index: number) => Key
```

This function is passed the index of each item and should return a unique key for that item. The default functionality of this function is to return the index of the item, but you should override this when possible to return a unique identifier for each item across the entire set.
This function is passed the index of each item and should return a unique key for that item. The default functionality of this function is to return the index of the item, but you should override this when possible to return a unique identifier for each item across the entire set.

**Important:** In React (and similar reactive frameworks), this function **must be memoized** (e.g., using `useCallback`) to prevent infinite re-render loops that will crash your application. Without memoization, the virtualizer will detect the function reference change on every render and trigger measurement recalculation, which causes another render, creating an infinite loop.
**Note:** The virtualizer automatically invalidates its measurement cache when measurement-affecting options change, ensuring `getTotalSize()` and other measurements return fresh values. While the virtualizer intelligently tracks which options actually affect measurements, it's still better to memoize `getItemKey` (e.g., using `useCallback` in React) to avoid unnecessary recalculations.

### `rangeExtractor`

Expand Down
5 changes: 0 additions & 5 deletions packages/virtual-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -645,11 +645,6 @@ export class Virtualizer<
},
{
key: false,
skipInitialOnChange: true,
onChange: () => {
// Notify when measurement options change as they affect total size
this.notify(this.isScrolling)
},
},
)

Expand Down
38 changes: 38 additions & 0 deletions packages/virtual-core/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,41 @@ test('should correctly recalculate lane assignments when lane count changes', ()
}).not.toThrow()
})
})

test('should update getTotalSize() when count option changes (filtering/search)', () => {
const virtualizer = new Virtualizer({
count: 100,
estimateSize: () => 50,
getScrollElement: () => null,
scrollToFn: vi.fn(),
observeElementRect: vi.fn(),
observeElementOffset: vi.fn(),
})

expect(virtualizer.getTotalSize()).toBe(5000) // 100 × 50

// Simulate filtering - reduce count to 20
virtualizer.setOptions({
count: 20,
estimateSize: () => 50,
getScrollElement: () => null,
scrollToFn: vi.fn(),
observeElementRect: vi.fn(),
observeElementOffset: vi.fn(),
})

// getTotalSize() should immediately return updated value (not stale)
expect(virtualizer.getTotalSize()).toBe(1000) // 20 × 50

// Restore full count
virtualizer.setOptions({
count: 100,
estimateSize: () => 50,
getScrollElement: () => null,
scrollToFn: vi.fn(),
observeElementRect: vi.fn(),
observeElementOffset: vi.fn(),
})

expect(virtualizer.getTotalSize()).toBe(5000) // 100 × 50
})
Loading