Changelog
What changed for users of active-virtual, per version.
@workspace/active-virtual 2.1.0Status unknowncontent differs from the recorded release 2.1.0baseline ce081dc3
The version line at the top of this page is read from the package and the release register of Foundation. A recorded release counts only for exactly the content it recorded: when the package changed after it, this site does not call that version released. The section Unreleased lists what changed since, including the changes made since Foundation maintains the package.
Notable changes to @workspace/active-virtual. Format based on
Keep a Changelog; versioning per
Semantic Versioning.
This file is the authoritative release history for @workspace/active-virtual.
The pre-1.0.0 entries (v0.1.0 → v0.2.8) document the internal iteration that
led to the v1.0.0 externally-publishable baseline and are preserved verbatim
in intent (internal tracking IDs and test-count tallies have been stripped —
the behavior change is what stays).
[Unreleased]#
Maintained in Foundation (since 2026-10-21)#
- Documentation: references to documents of the source repository are
text, marked
(source repository: …), instead of links that resolve nowhere here. No code changed.
[2.1.0] — 2026-10-18#
Changed — canonical transfer form (2026-10-18, platform ADR-112)#
- The manifest is the transferable manifest (RPS-D7): no workspace
package in
devDependencies(what the package's own development takes from Datyx, such as the lint preset, is host tooling in the reusable package register); the declaration packages of what the source imports sit in the field of their module (@types/reactand@types/react-domas optional peers beside thereactpeers). Nothing changes for a Datyx consumer. tsconfig.jsonis self-contained: it no longer extends@workspace/typescript-config, with the same compiler options, because a bundler in a consumer reads it for every source file it compiles.
Added (portable runtime P6, D-12)#
toLogical,toPhysicalandScrollPlaneare exported from the root entry: the one conversion between a physical scroll offset and the logical offset, for a consumer that reads or writesscrollLeftbeside an axis. Unchanged behaviour.
Fixed#
-
A request whose write landed short before the layout caught up is written again. When an instant write of a request to an item or an edge lands short because the container had not laid out a grown extent yet, the browser adapter writes the same target once more as soon as the container's scroll range has grown, checked at that request's own ticks. Only the same, still active request with the same target is served, only on growth and never because time passed; the write and visible-time budgets run on unchanged, and a target that stays out of reach gives up as before (
DECISIONS.mdD-13). No API change. -
An instant scroll write the browser clamps no longer leaves the axis at the requested offset. After every instant write the browser adapter gives the axis the offset the browser actually reached, also when the write moved nothing and no scroll event follows (a container already at its physical end, or with no scroll range). The projection now shows what the viewport shows, and such a request never settles on the offset it asked for: it keeps its target, warns
scroll-unreachable, and gives up at its budget. -
A request's pending write no longer lands after the request has ended. A request that is superseded, interrupted, loses its target or gives up before its write reaches the browser now writes nothing, also when the newer request writes nothing itself (for example one for a missing key). A compensation the withdrawn write had replaced still lands, and the other axis's writes are untouched; the axis returns to the offset the container keeps.
-
Measurements held back during a smooth request no longer regain withdrawn authority. A held measurement is withdrawn by a newer accepted measurement of the item, an invalidation of it (
staleorvoid), a reset, or the item leaving the collection. It no longer returns after avoidinvalidation, never applies to a key that was removed and inserted again, and never overwrites a newer measurement. -
A running smooth scroll animation no longer outlives its request. When a newer request supersedes a smooth request whose animation the browser is still running, the browser adapter stops that animation, also when the newer request writes nothing (a missing item, or
autoto an item already visible); the viewport stays at the offset the newer request was resolved against instead of drifting to the old target. The same holds when a collection change removes the request's target and when input interrupts it, including in Firefox, which kept animating through a write to the current position. Only the ended request's direction stops: the other axis's smooth request carries on and lands. An animation whose request settles or gives up, or that the user takes over by moving the container, is left alone.
Documentation#
-
The ROADMAP holds no open package work. What stood there as open choices is recorded as current contract or envelope (
DECISIONS.mdD-14): an active request across a container switch (now also pinned by a test), the 2 000-unit visible-time budget for long smooth scrolls, and the O(n) key index for keyed edits. Evidence and decisions owned elsewhere moved to active-grid: the Safari RTL measurement (R-61) and the legal question about versions before 2.0.0 (TD-42). -
Projection.visibleis described as it behaves: with one lane every position between its bounds intersects the viewport, except that a viewport edge in a gap, a padding or beyond the content takes in the nearest item on that side.API.mdand the TSDoc said every position between them was visible, which a layoutgapcontradicted. No runtime change; the rule is now pinned inrange.test.ts. -
The 1.x → 2.0.0 migration table moved from
API.mdinto the 2.0.0 entry below:API.mdcarries the current contract only. -
ARCHITECTURE.mdstates the actual cost of a scroll: every offset change rebuilds the rendered items and compares the projection by value, O(w) on top of the lane searches, and sorts the merged items when there is more than one lane. A key sequence rebuilds the key index and the size register directly (O(n)); edits are derived only to follow a removed anchor.API.mdno longer says the engine derives the edits of a key sequence. No runtime change. -
API.mdstates the exact rules for held measurements, a reused key, a withdrawn pending write and a write that lands short, instead of the absolute "never dropped" and "never counts as reached" claims. -
THIRD-PARTY-NOTICES.mdis removed. It carried the TanStack Virtual (MIT) notice for the engine of the versions before 2.0.0; that engine and all of its source were removed in 2.0.0, and a provenance review of the shipping engine supported its independence (docs/DECISIONS.mdD-8). This is not a legal opinion. -
The build-time documents of the independent rewrite (product contract, programme architecture, acceptance baseline, research) are removed; their durable content is in
API.md,ARCHITECTURE.md,FUNCTIONEEL-OVERZICHT.mdandDECISIONS.md.API.mdnow states the guarantees and measured limits per browser itself.
[2.0.0] — 2026-09-25#
Breaking — a new engine and API#
- The engine is replaced. The package ships one, independently written engine: a framework-neutral, DOM-independent core per axis, a browser scroll host, and a React adapter. The previous engine, its React hooks and its whole API are removed; nothing of the 1.x API remains.
- Root entry:
createAxisStore,createScrollHost,findScrollHostand the value types (AxisInit,AxisInput,Projection,ProjectedItem,AnchorPolicy,RangePolicy,Diagnostic, …). ./react:useVirtualAxis, one hook per axis, returning the projection of the render, a controller with a stable identity,itemRefandisRendered;inspectAxisfor lifecycle tests.
Added#
- Keyed identity with explicit edits and key sequences; measurements follow their item.
- Anchoring through every geometry change (start edge, end edge, keyed, focus, none, or a function per change) and edge follow for end-oriented lists; a removed anchor is replaced by its nearest surviving neighbour.
- Explicit invalidation (
stale,void) and reset with collection epochs; a measurement for an old epoch or an unknown item is dropped. - Range observation (
observeRange,onRangeChange) andisRendered. - Focus custody in the React adapter (opt-in).
- An explicit pre-measurement status (
awaiting-viewport) that is identical on the server and the first client render; seeded server windows. - A browser adapter that merges the scroll writes of one turn per container, recognises its own writes, and reports a write the browser cannot reach.
Changed#
- Scroll requests and compensations are clamped to the declared extent;
content that follows the list inside the container is declared as
paddingEnd, content before it aslistOrigin. - A measurement equal to an item's current size is kept and survives a later estimate change.
- Diagnostics are structured (
code,message,data) and go to one sink.
Migration#
Replace useVirtualizer / useWindowVirtualizer with useVirtualAxis,
estimateSize with estimate, getVirtualItems() / getTotalSize() with
projection.items / projection.extent, ref={measureElement} plus
data-index with ref={itemRef(identity)}, measure() with
controller.reset(), and scrollToIndex with controller.scrollToPosition.
The full table:
| 1.x | 2.0.0 |
|---|---|
use | use |
horizontal: true | orientation: 'horizontal' |
get | keys (keyed identity) instead of count |
get, get | projection., projection. |
Virtual, . | Projected, . |
ref={measure with data- | ref={item; no attribute |
measure() | controller., or controller. |
scroll | controller. |
on, on | on |
use | use |
should | anchor (a choice, or a function of the geometry change) |
range | range |
initial, initial, initial | seed: {viewport |
new Virtualizer({...}) with observers | create and create |
| Clamped to the element's physical scroll range | Clamped to the declared extent; declare trailing content as padding (D-6) |
Unreleased changes to the replaced engine (superseded by 2.0.0)#
These entries describe the 1.x engine after 1.0.1; they were never released on their own, and 2.0.0 removed that engine.
Fixed — standards convergence#
getVirtualItemForOffsetreturns an item that covers the offset when lanes have different heights: starts do not rise with the index across lanes, so each lane is searched on its own (the lowest covering index wins; in a gap, the nearest item below). A single lane is unchanged.getTotalSize()of an empty list keepspaddingStartandpaddingEndwhen ascrollMarginis set (it returned0).
Documentation — standards convergence#
- The range contract says precisely that in one lane the first and last item
of
calculateRange()may lie just outside the viewport when the offset falls in agapor past the last item. Scale and browser claims narrowed to what is measured (operation counts up to 100,000 items; one recorded T6 run).
Documentation#
- License and attribution. active-virtual is proprietary and
workspace-private (
LICENSE,UNLICENSED). The MIT notice of TanStack Virtual, from which it is derived, is now reproduced inTHIRD-PARTY-NOTICES.md; README and docs no longer call the package MIT. - The documentation describes the current engine and the current public API, and no longer carries the v1.0.0 external-publication promise: the package is not published, and its API is what the two entry points export.
Fixed#
-
Measurements are recomputed incrementally. A read recomputes only from the first item whose measurement became invalid; a read with nothing invalid does no work. Previously every internal read rebuilt every item, so a scroll event cost O(count) (about 5 ms per event at 100 000 items) and measuring a mounted window cost O(count) per measured item. Unchanged reads no longer depend on the item count; neither do scroll events, in one lane or in several (see below for strongly uneven lanes).
-
Zero-size items no longer drop out of the rendered range. Items of size 0 that share a start position with their neighbours stay in the range, so they are measured again once they have a size.
-
A pre-mount render no longer stays on screen when mounting empties the range. Render-skipping compares against the index set the subscriber last read, not the set last notified.
-
The total size frozen during a scroll follows a change of
count. -
A smooth
scrollToIndexno longer interrupts itself. Every scroll event of the animation between the previous position and the target belongs to the programmatic scroll; only a movement away from the target, or past it, is a user interruption. Reconciliation waits while a smooth scroll progresses instead of re-issuing it every other frame, so deferred measurements are applied on arrival rather than after the second frame. -
Cleanup is a hard boundary. The scroll-end timer and the animation frames of the container and item observers are cancelled on cleanup and on a scroll-element swap, and callbacks of an older subscription are ignored, so no late work writes into an unmounted or re-subscribed instance. The container observer's frame now runs on the owning window (iframe-aware).
-
Cleanup no longer constructs a
ResizeObserver. Releasing an element or disconnecting when no item observer exists does nothing. -
Options changed through
setOptionstake effect on the mounted element.useAnimationFrameWithResizeObserveris read at every resize;isScrollingResetDelayanduseScrollendEventre-subscribe the offset observer at the next_willUpdate()without replacing the scroll element. -
Horizontal RTL scrolling writes as it reads.
scrollToOffset,scrollBy,scrollToIndexand resize corrections now use the same logical offset as the offset observer; the built-in element and window observers map it to the negative RTLscrollLeft/scrollXof CSSOM View. -
Changing
horizontalno longer keeps sizes measured along the old axis. The change drops the old-axis sizes and scroll state at once; the next_willUpdate()re-subscribes, writesinitialOffsetalong the new axis and measures the mounted items again. -
A page hidden during
scrollToIndexno longer aborts it on return. Time the owning document is hidden no longer counts toward the reconciliation timeout, so the scroll continues instead of ending with a spurious timeout warning and an early flush of deferred measurements. -
The latest scroll command wins.
scrollToOffsetandscrollBynow end a pendingscrollToIndexreconciliation, as a newerscrollToIndexalready did, so an older reconciliation no longer drags the list back to its index. A user scroll that the runningscrollToIndexcannot explain now ends it in instant mode too, not only in smooth mode. -
A window from another realm is virtualized. The owner window of a window scroll element is resolved without
instanceof Window, so an iframe'scontentWindowgets its scroll listener, timers and frames. -
A measurement replaces an
initialMeasurementsCacheseed for good. The seeds are applied once, before the first computation. Previously an invalidation from index 0 (a measurement of item 0, or any structural option change) applied them again and overwrote newer measured sizes.measure()andenabled: false, which discard every measurement, still start from the seeds again. -
The newest scroll command is the only writer, also under re-entry. A newer command, a cleanup or a scroll-element swap issued synchronously inside a
scrollToIndex's own write (a customscrollToFnthat reports the offset at once) or insideestimateSize/getItemKeywhile it computes its target now ends thatscrollToIndexat once. Previously its reconciliation scheduled one more verification and wrote its old target over the newer command. -
A command issued from any callback of an older command wins. Every command now owns the scroll target before it ends the previous one, and checks its ownership after each consumer callback. A newer command issued from
onChange(while deferred measurements of the previous command apply), from the smooth-scrollonWarnofscrollToOffset/scrollBy, fromestimateSizein a laterscrollToIndexverification, or fromonDebug('Schedule retry')is no longer overwritten by the older command. Previously the older command still wrote its target, a later verification threw on its cancelled state, or a retry scheduled an obsolete frame. The smooth-scroll warning ofscrollToOffset/scrollBynow follows the end of the previous command. -
Applying deferred measurements stops when ownership passes on. A batch of measurements a smooth
scrollToIndexdeferred is the work of the command that flushes it, and its own callbacks reach consumer code:measureElement,shouldAdjustScrollPositionOnItemSizeChange, thecorrectiondebug notification,onChange, andestimateSize/getItemKeywhile a later entry is looked up. A command issued from any of them now takes the scroll target over at once and the rest of the batch is dropped — no further correction, no size recorded, no notification — at a command's start, at a user interruption and at the settlement of a reconciliation alike. Previously the batch ran on: a two-entry flush wrote[230, 3000, 260]where the newer command's 3000 had to be last, a settled reconciliation wrote[5030, 3000, 5060], and a single entry whose own callback replaced the owner still wrote its stale correction after it ([3000, 230]). Measurement that belongs to no command (measureElement()from a ref,resizeItem(),measure()) is unchanged. -
align: 'end'aligns the requested item in a multi-lane layout. The scroll-to-the-very-end rule (maximum offset,paddingEndin view) now applies to the item whose end is the end of the content, not to the last index; in several lanes an earlier item can reach further, and the last index then aligns by its own end. The result is also clamped at 0 when the content is shorter than the viewport. -
The frozen total size follows every layout change. During a scroll episode
getTotalSize()was re-based only whencountchanged. It is now re-based on any structural change:gap,paddingStart,paddingEnd,scrollMargin,lanes,horizontal,enabled, a newestimateSizeorgetItemKeyfunction (an active-grid column resize mid-scroll), ormeasure(). Measured size changes stay frozen as before. -
Multi-lane totals and placement no longer scan the list. An unchanged
getTotalSize()read one item per lane at best but, in strongly uneven lanes, every item back to the start (100 000 inspections per read at 100 000 items); placing an item and resuming after an invalidation scanned backward the same way, so building such a layout was quadratic. Per-lane bookkeeping now makes an unchanged read one inspection per lane, an append a handful, and a recomputation the changed suffix plus the affected lanes.
Changed#
- Breaking: visibility is per lane, and the engine applies the overscan.
With
lanes > 1every lane is searched in its own geometry, and the rendered set is each lane's visible run plus its own directional overscan, merged in ascending index order. It need not be contiguous by index: a strongly uneven (masonry) layout renders only the visible items of each lane and their overscan, however far apart their indexes are, at O(lanes × log count) plus the visible items per scroll event. Previously one index interval covered every index between the lowest and highest visible item and was expanded until every lane passed the viewport, which in the extreme rendered the whole list at O(count) per scroll event.overscannow counts items per lane (roughly rows) instead of indexes. One lane renders exactly what it rendered before. - Breaking:
Rangegainsindexes, the default render set (visible items plus per-lane overscan, ascending).startIndexandendIndexare the lowest and highest visible index; in several lanes they are bounds only, as iscalculateRange(). The defaultrangeExtractorreturnsrange.indexes; a custom extractor extends that set instead of computing overscan. - A new identity of
estimateSizeorgetItemKeyis now the signal to recompute every item; a function that keeps its identity is treated as pure. Keep both stable and pass a new function when estimates or keys change. - Measured sizes of keys the list no longer produces are dropped once the size cache holds more sizes than there are items.
- Size and count contract.
countmust be a non-negative integer, or the constructor andsetOptionsthrowRangeError. AnestimateSizeresult must be finite and>= 0, or the read that needs it throwsRangeError. An invalid measured size (measureElement,resizeItem) is reported throughonWarnand ignored. Zero and fractional sizes stay valid. - Layout option contract.
lanesmust be an integer>= 1;gap,paddingStart,paddingEnd,scrollPaddingStart,scrollPaddingEndandscrollMarginmust be finite and>= 0. An invalid value throwsRangeErrorfrom the constructor orsetOptions, and a rejectedsetOptionskeeps the previous options. initialMeasurementsCachesizes obey the size contract. A seeded size that is negative,NaNor infinite throwsRangeErrorwhen the cache is seeded. Onlykeyandsizeare read from the cache.- Changing
isScrollingResetDelayoruseScrollendEventduring a scroll ends the running scroll episode (isScrollingbecomesfalse). - Explicit public surface. Both entry points name every export; nothing is
re-exported by wildcard. A contract test pins the export lists (types
included), the export map and the public members of
Virtualizer. ./reactexportsVirtualizeras a type only. Construct aVirtualizerfrom the root entry.- Read-only shared shapes (type level, no runtime freeze). The fields of
VirtualItemandRangearereadonly;getVirtualItems()returnsreadonly VirtualItem[]andgetVirtualIndexes()readonly number[], because both arrays are memoized and the items are the virtualizer's cached measurements.rangeExtractormay return areadonly number[],initialMeasurementsCacheaccepts areadonly VirtualItem[], and the members ofVirtualizerLikearereadonly. Code that mutated a returned item or array no longer compiles. calculateRange,getVirtualIndexesandgetVirtualItemsare typed as plain functions; the internal memo helper'supdateDepsis no longer part of their type.elementScroll,windowScroll,measureElementand the four observers are generic over the item element type instead of acceptinganythere, so they are assignable to anyVirtualizerOptionswithout a cast.
Added#
VirtualizerLikeis exported from the root entry: the instance as the pluggable functions (scrollToFn, the observers,measureElement,onChange) receive it, for typing a custom one../reactexportsUseVirtualizerOptionsandUseWindowVirtualizerOptions, the option types of the two hooks, andAdaptiveOverscanOptions.
Removed#
- Breaking: root exports that were implementation helpers, public only
through wildcard re-exports:
approxEqual,debounce,defaultKeyExtractor,findNearestBinarySearch,getElementRect,indexesEqual,memo,MemoizedFunction,MemoOptions,notUndefined,resolveOverscan,PartialKeysandScrollToOptions(still the shape behindScrollToIndexOptionsandScrollToOffsetOptions).debounceitself is deleted: nothing called it since the scroll-end timer became cancellable. - Breaking:
./reactno longer exportsReactVirtualizerOptions, an alias ofVirtualizerOptions(useVirtualizerOptionsor the hook option types), orPartialKeys. - Breaking:
defaultRangeExtractor. The default render set, overscan included, isRange.indexes; a customrangeExtractorextends it. - Breaking: the
Virtualizerinstance membersrange(usecalculateRange()) andelementsCache(internal bookkeeping of the measured elements).
[1.0.1] — 2026-05-20#
Internal type-cleanup. Reduces @typescript-eslint/no-explicit-any warnings from 14 → 0 across the package via two centralized type-aliases and a handful of motivated eslint-disable directives. No API surface changes; no runtime behaviour changes.
Changed (internal)#
- New internal
ScrollObserverInstance<T>andItemMeasurementInstance<T>type-aliases incore/virtualizer/types.ts. They consolidate theVirtualizerLike<T, any>/VirtualizerLike<any, T>pattern used by observer-functions where one of the two generic parameters is irrelevant to the caller. Theanyin the unused slot is required becauseVirtualizerLikeis invariant in both parameters (TItemElementappears in parameter and return positions ofoptions.measureElement/options.onChange), so narrowing toElementwould reject valid downstream-typed instances. Motivatedeslint-disablelives on the alias-definitions with JSDoc rationale. core/observers/element-observers.tsusesScrollObserverInstance<T>forobserveElementRect,observeElementOffset, andelementScroll.core/observers/window-observers.tsusesScrollObserverInstance<Window>/ScrollObserverInstance<T>forobserveWindowRect,observeWindowOffset, andwindowScroll.core/observers/measure-element.tsusesItemMeasurementInstance<TItemElement>formeasureElement.core/utils/memo.tsintroduces a file-internalAnyDepsTuple = ReadonlyArray<any>alias for variadic-tuple-inference onTDeps(TS #28259). Singleeslint-disableon the alias-definition covers all three call-sites (MemoOptions,MemoizedFunction,memofunction-generic). Internal storage variabledepsswitched fromReadonlyArray<any>→ReadonlyArray<unknown>(the storage role doesn't need the variadic-tuple semantics). Removed explicit: anyannotation from the.some()callback parameter (TypeScript infers fromTDeps).core/utils/helpers.tsdebounceretains its variadic-tuple-inference pattern (TArgs extends Array<any>) and transparentthis-forwarding (this: any), now with motivatedeslint-disabledirectives explaining each.
Not changed#
- Public API surface: no changes to
Virtualizer, observer functions,measureElement,memo, or any exported type/function shape. - Runtime behaviour: no changes.
- Downstream
@workspace/active-gridconsumer still typechecks clean.
Tests#
- All 221 tests remain green (no behaviour changes, no test additions).
[1.0.0] — 2026-05-19#
First stable major release. Promotes the 0.2.8 surface to a stable, externally-publishable baseline with an API-stability promise from this version onward.
No API changes versus 0.2.8 — this release is the version-promotion itself.
Notes for consumers#
- Import paths, surface, and runtime behaviour are unchanged from 0.2.8.
- The README license block has been corrected to match the
LICENSEfile (MIT — TanStack-Virtual-derivation). The previous "All rights reserved" wording was misleading documentation, not a license change.- Correction, 2026-09-19: this no longer describes the package. Its
LICENSEis the proprietary Datyx license andpackage.jsonsaysUNLICENSED; active-virtual is not MIT-licensed. The MIT notice of TanStack Virtual, from which the versions before 2.0.0 were derived, was carried inTHIRD-PARTY-NOTICES.mduntil that engine was removed. The entry above is kept as it was released.
- Correction, 2026-09-19: this no longer describes the package. Its
- The
src/index.tspackageDocumentation now states "React 19 compatible" only, matchingpackage.jsonpeerDeps (^19.0.0) and the README claim. - Full package documentation now lives under
docs/; thisCHANGELOG.mdandDECISIONS.mdmoved from the package root intodocs/as part of the same restructure.
[0.2.8] — 2026-04-06#
Fixed#
measure()clears deferred measurements and cancels smooth scroll.measure()now clears_deferredMeasurements, cancels the active_cancelScrollToIndex, and resets_smoothScrollState. Previously, ameasure()call during an active smooth scroll (triggered by a cross-package sort/filter flow inactive-grid) could leave stale deferred measurements that would be applied after the reset.
[0.2.7] — 2026-04-06#
Fixed#
- EMA velocity no upper bound. Added
MAX_VELOCITYcap (10 000 px/s) after the EMA update in_updateScrollVelocity(). macOS momentum scrolling can reach 50k–100k px/s, which would produce thousands of overscan rows. The cap prevents extreme overscan while preserving smooth velocity tracking for normal scroll speeds.
[0.2.6] — 2026-04-06#
Fixed#
- EMA velocity one-frame blank after scroll pause. Velocity tracking
no longer hard-resets
_lastScrollOffset,_lastScrollTime, and_scrollVelocityto 0 when scrolling stops. Instead, tracking state is preserved and velocity decays gradually viaMath.pow(0.5, dt / 100)when the next scroll event arrives after a pause > 100 ms. Prevents a one-frame blank flash (overscan = 0) when rapidly resuming scroll after a brief pause. - User scroll interrupt now cancels reconciliation. When a
user-initiated scroll is detected during a smooth
scrollToIndexanimation, the reconciliation RAF loop is now cancelled (_cancelScrollToIndex) in addition to flushing deferred measurements. Previously, the RAF loop continued running after interrupt.
[0.2.5] — 2026-04-06#
Fixed#
- scrollToIndex cancel/new race condition.
scrollToIndex()now calls_flushDeferredMeasurements()before cancelling the previous reconciliation and setting new smooth scroll state. Previously, deferred measurements from the previous scroll were lost because_smoothScrollStatewas overwritten and_deferredMeasurementscleared beforecancel()was called. Thecancel()function skipsonSettled, so deferred measurements were never flushed.
[0.2.4] — 2026-04-05#
Tests#
- Explicit
cancel()unmount behavior. Test verifies thatcancel()preventsscrollToFnfrom being called after the virtualizer is cleaned up. Dynamic mode with changing offsets — aftercancel(), no further scroll attempts are made andonSettledis not called.
[0.2.3] — 2026-04-05#
Fixed#
- RAF cancellation on unmount.
performScrollToIndexnow returns acancel()function that cancels pendingrequestAnimationFramecallbacks. TheVirtualizercallscancel()in itscleanup()method on unmount, preventingscrollToFnfrom being called on disconnected elements. Previous scroll-to-index reconciliations are also cancelled when a newscrollToIndexcall is made.
[0.2.2] — 2026-04-04#
Breaking Changes#
useFlushSyncoption removed fromReactVirtualizerOptions— was accepted but never implemented.
Fixed#
elementsCacheis now cleaned up onmeasure()— prevents stale element references after item removal.
Documentation#
- SSR usage with
initialMeasurementsCachedocumented in README. - Next.js App Router example added.
[0.2.1] — 2026-04-04#
Fixed#
useVirtualizersnapshot bug.useSyncExternalStorereceived the sameVirtualizerclass-instance reference as snapshot, causingObject.is()to always returntrueand preventing re-renders in Concurrent Mode — fixed by returning a_stateVersioncounter that increments on everynotify().- Velocity tracking jojo-effect. Sliding-window velocity calculation
returned 0 during rapid direction changes (e.g. 200 px down then
200 px up within 100 ms) — replaced with Exponential Moving Average
(
alpha = 0.3) using absolute instant velocity. - Smooth scroll interrupt. User-initiated scroll during a smooth
scrollToIndexanimation left deferred measurements waiting for the 2000 ms timeout — now detects user interrupts via_programmaticScrollPendingflag and flushes deferred measurements immediately.
Changed#
- Velocity tracking: replaced sliding-window array with EMA (two private fields instead of array allocation, better spike filtering).
- Default
velocityFactorlowered from 0.05 to 0.01 — produces a smoother gradient between min and max overscan instead of binary behavior. - Default scroll-reconciliation timeout lowered from 5000 ms to 2000 ms — 2 seconds is sufficient for DOM settling, 5 seconds felt like frozen UI.
- Velocity unit is now px/s (was px/ms internally).
[0.2.0] — 2026-04-04#
Fixed#
- Scroll reconciliation: added 5000 ms safety timeout and stability-frame check — prevents infinite loops in edge cases (broken layout, zero-height containers).
indexFromElement: addedNaNhandling for non-numericdata-indexattributes.
Added#
resizeItem(index, size)method — programmatic resize without DOM measurement, with scroll adjustment for items above viewport and bounds checking with dev-mode warnings.- Scroll-frame optimization — skips React re-renders when scroll offset changes but visible items remain the same, comparing full virtual index arrays.
- Directional overscan — asymmetric overscan based on scroll direction (70 % in scroll direction, 30 % opposite), minimum 1 in each direction.
- Adaptive overscan — velocity-based dynamic overscan via
AdaptiveOverscanOptions({min, max, velocityFactor}). Fast scrolling increases overscan, idle reduces to minimum. Integrates with directional overscan (adaptive = how much, directional = where). - Smooth-scroll measurement throttling — during smooth
scrollToIndex, measurements are limited to items within a buffer around the target index. Deferred measurements are flushed when reconciliation settles. initialOffsetoption documentation and edge-case handling.indexFromElement()method documentation.calculateRange()public-method documentation.
Changed#
overscanoption now acceptsnumber | AdaptiveOverscanOptions(backwards compatible).defaultRangeExtractornow applies directional overscan based onscrollDirectionproperty onRange.- Scroll-reconciliation state includes
startedAttimestamp andstableFramescounter. - Virtualizer tracks scroll velocity via EMA (used by adaptive overscan).
[0.1.0] — Initial release#
- Virtualizer core with vertical/horizontal support.
- Multi-lane (grid) layout with
lanesoption. - Dynamic item measurement via
ResizeObserver. - Scroll to index/offset with alignment options.
useVirtualizeranduseWindowVirtualizerReact hooks.- SSR support with
initialMeasurementsCache.