docs/solutions/performance-issues/2026-05-03-slate-tanstack-virtual-items-must-not-be-memoized.md
TanStack Virtual owns a live scroll/measurement state. Wrapping
virtualizer.getVirtualItems() in a useMemo can freeze Slate's virtualized
rendering plan at the initial viewport even though the virtualizer receives
scroll events.
virtualized block 1000.virtualizerMeasuredCount.Compute the Slate virtualized plan from the current virtualizer state on each
render. Keep stable configuration in useMemo, but do not memoize the
getVirtualItems() snapshot.
const virtualizer = useVirtualizer({
count,
estimateSize: () => estimatedBlockSize,
getItemKey: index => topLevelRuntimeIds[index] ?? index,
getScrollElement: () => rootElement,
overscan,
rangeExtractor,
})
const virtualItems = virtualizer.getVirtualItems().map(item => ({
index: item.index,
key: item.key,
runtimeId: topLevelRuntimeIds[item.index]!,
size: item.size,
start: item.start,
}))
Do not hide that snapshot inside:
React.useMemo(() => virtualizer.getVirtualItems(), [virtualizer])
The virtualizer object can stay stable while its internal range changes.
TanStack Virtual triggers React updates as scroll/measurement state changes. Slate has to re-read the current virtual range during those renders. Memoizing the derived Slate plan on the virtualizer instance identity keeps stale viewport items alive and makes scrolling look broken.
virtualizer.getVirtualItems() like a live external-store snapshot.
Read it during render; do not memoize it by virtualizer identity.