🐛 Bug Report: “Maximum update depth exceeded” when using onChange with Virtualizer
维护者通常 2 天内回复
还没有人认领这个 Issue。
评估
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 新手友好度
- 45/100
- Issue 类型
- 缺陷
- 描述清晰度
- 需要澄清
- 活跃度
- 冷清
- 技术栈
- react, typescript
- 领域
- frontend, performance
调研方向
从报告中的 useVirtualizer 配置以及经过 Virtualizer.notify、resizeItem 和 _measureElement 的调用栈开始;通过更改外部容器的尺寸来复现故障。报告没有指出任何仓库源文件或测试,因此请先定位 React virtualizer 的实现和现有的 resize 测试。完成标准是调整大小不会产生 maximum-update-depth 循环,同时正常的虚拟化行为保持不变。
由索引模型根据 Issue 内容生成。
描述
Describe the bug
🐛 Bug Report: “Maximum update depth exceeded” when inner using onChange with Virtualizer
Package: @tanstack/react-virtual
Version: (e.g. 3.x.x — please replace with your actual version)```js
🧩 Description
Note: I did not provide an onChange prop to the Virtualizer.
The infinite update loop is triggered when the outer container’s dimensions change (for example, when resizing the browser window or the parent element).
When using the onChange callback in useVirtualizer, I encountered a “Maximum update depth exceeded” error caused by an apparent infinite update loop.
It seems that when the Virtualizer calls onChange → my component updates React state → this triggers a re-measure or resize → Virtualizer calls onChange again, leading to an infinite render cycle.
Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
at throwIfInfiniteUpdateLoopDetected (react-dom.development.js:26793:11)
at getRootForUpdatedFiber (react-dom.development.js:7627:3)
at enqueueConcurrentHookUpdate (react-dom.development.js:7518:10)
at dispatchReducerAction (react-dom.development.js:12998:16)
at Object.onChange (index.js:37:9)
at Virtualizer.notify (index.js:281:65)
at Virtualizer.resizeItem (index.js:563:14)
at Virtualizer._measureElement (index.js:541:14)
at Virtualizer.measureElement (index.js:576:12)
at commitAttachRef (react-dom.development.js:21677:37)
at safelyAttachRef (react-dom.development.js:20803:5)
使用的代码
```jsx
const contentRef = useRef<HTMLDivElement>(null);
const contentSize = useSize(contentRef);
const virtualOptions = useMemo(() => ({
count: list.length,
getScrollElement: () => scrollbarRef.current?.getScrollElement() || null,
estimateSize: () => 36,
overscan: 45,
scrollMargin: contentSize?.height || 0,
enabled: true,
}), [
contentSize?.height,
list.length,
]);
const rowVirtualizer = useVirtualizer(virtualOptions);
const virtualItems = rowVirtualizer.getVirtualItems();
const getRect = (element) => {
const {offsetWidth, offsetHeight} = element;
return {
width: offsetWidth,
height: offsetHeight
};
}
;
const defaultKeyExtractor = (index) => index;
const defaultRangeExtractor = (range) => {
const start = Math.max(range.startIndex - range.overscan, 0);
const end = Math.min(range.endIndex + range.overscan, range.count - 1);
const arr = [];
for (let i = start; i <= end; i++) {
arr.push(i);
}
return arr;
}
;
const observeElementRect = (instance, cb) => {
const element = instance.scrollElement;
if (!element) {
return;
}
const targetWindow = instance.targetWindow;
if (!targetWindow) {
return;
}
const handler = (rect) => {
const {width, height} = rect;
cb({
width: Math.round(width),
height: Math.round(height)
});
}
;
handler(getRect(element));
if (!targetWindow.ResizeObserver) {
return () => {}
;
}
const observer = new targetWindow.ResizeObserver( (entries) => {
const run = () => {
const entry = entries[0];
if (entry == null ? void 0 : entry.borderBoxSize) {
const box = entry.borderBoxSize[0];
if (box) {
handler({
width: box.inlineSize,
height: box.blockSize
});
return;
}
}
handler(getRect(element));
}
;
instance.options.useAnimationFrameWithResizeObserver ? requestAnimationFrame(run) : run();
}
);
observer.observe(element, {
box: "border-box"
});
return () => {
observer.unobserve(element);
}
;
}
;
const addEventListenerOptions = {
passive: true
};
const observeWindowRect = (instance, cb) => {
const element = instance.scrollElement;
if (!element) {
return;
}
const handler = () => {
cb({
width: element.innerWidth,
height: element.innerHeight
});
}
;
handler();
element.addEventListener("resize", handler, addEventListenerOptions);
return () => {
element.removeEventListener("resize", handler);
}
;
}
;
const supportsScrollend = typeof window == "undefined" ? true : "onscrollend"in window;
const observeElementOffset = (instance, cb) => {
const element = instance.scrollElement;
if (!element) {
return;
}
const targetWindow = instance.targetWindow;
if (!targetWindow) {
return;
}
let offset = 0;
const fallback = instance.options.useScrollendEvent && supportsScrollend ? () => void 0 : (0,
_utils_js__WEBPACK_IMPORTED_MODULE_0__.debounce)(targetWindow, () => {
cb(offset, false);
}
, instance.options.isScrollingResetDelay);
const createHandler = (isScrolling) => () => {
const {horizontal, isRtl} = instance.options;
offset = horizontal ? element["scrollLeft"] * (isRtl && -1 || 1) : element["scrollTop"];
fallback();
cb(offset, isScrolling);
}
;
const handler = createHandler(true);
const endHandler = createHandler(false);
endHandler();
element.addEventListener("scroll", handler, addEventListenerOptions);
const registerScrollendEvent = instance.options.useScrollendEvent && supportsScrollend;
if (registerScrollendEvent) {
element.addEventListener("scrollend", endHandler, addEventListenerOptions);
}
return () => {
element.removeEventListener("scroll", handler);
if (registerScrollendEvent) {
element.removeEventListener("scrollend", endHandler);
}
}
;
}
;
const observeWindowOffset = (instance, cb) => {
const element = instance.scrollElement;
if (!element) {
return;
}
const targetWindow = instance.targetWindow;
if (!targetWindow) {
return;
}
let offset = 0;
const fallback = instance.options.useScrollendEvent && supportsScrollend ? () => void 0 : (0,
_utils_js__WEBPACK_IMPORTED_MODULE_0__.debounce)(targetWindow, () => {
cb(offset, false);
}
, instance.options.isScrollingResetDelay);
const createHandler = (isScrolling) => () => {
offset = element[instance.options.horizontal ? "scrollX" : "scrollY"];
fallback();
cb(offset, isScrolling);
}
;
const handler = createHandler(true);
const endHandler = createHandler(false);
endHandler();
element.addEventListener("scroll", handler, addEventListenerOptions);
const registerScrollendEvent = instance.options.useScrollendEvent && supportsScrollend;
if (registerScrollendEvent) {
element.addEventListener("scrollend", endHandler, addEventListenerOptions);
}
return () => {
element.removeEventListener("scroll", handler);
if (registerScrollendEvent) {
element.removeEventListener("scrollend", endHandler);
}
}
;
}
;
const measureElement = (element, entry, instance) => {
if (entry == null ? void 0 : entry.borderBoxSize) {
const box = entry.borderBoxSize[0];
if (box) {
const size = Math.round(box[instance.options.horizontal ? "inlineSize" : "blockSize"]);
return size;
}
}
return element[instance.options.horizontal ? "offsetWidth" : "offsetHeight"];
}
;
const windowScroll = (offset, {adjustments=0, behavior}, instance) => {
var _a, _b;
const toOffset = offset + adjustments;
(_b = (_a = instance.scrollElement) == null ? void 0 : _a.scrollTo) == null ? void 0 : _b.call(_a, {
[instance.options.horizontal ? "left" : "top"]: toOffset,
behavior
});
}
;
const elementScroll = (offset, {adjustments=0, behavior}, instance) => {
var _a, _b;
const toOffset = offset + adjustments;
(_b = (_a = instance.scrollElement) == null ? void 0 : _a.scrollTo) == null ? void 0 : _b.call(_a, {
[instance.options.horizontal ? "left" : "top"]: toOffset,
behavior
});
}
;
class Virtualizer {
constructor(opts) {
this.unsubs = [];
this.scrollElement = null;
this.targetWindow = null;
this.isScrolling = false;
this.measurementsCache = [];
this.itemSizeCache = /* @__PURE__ */
new Map();
this.pendingMeasuredCacheIndexes = [];
this.scrollRect = null;
this.scrollOffset = null;
this.scrollDirection = null;
this.scrollAdjustments = 0;
this.elementsCache = /* @__PURE__ */
new Map();
this.observer = /* @__PURE__ */
( () => {
let _ro = null;
const get = () => {
if (_ro) {
return _ro;
}
if (!this.targetWindow || !this.targetWindow.ResizeObserver) {
return null;
}
return _ro = new this.targetWindow.ResizeObserver( (entries) => {
entries.forEach( (entry) => {
const run = () => {
this._measureElement(entry.target, entry);
}
;
this.options.useAnimationFrameWithResizeObserver ? requestAnimationFrame(run) : run();
}
);
}
);
}
;
return {
disconnect: () => {
var _a;
(_a = get()) == null ? void 0 : _a.disconnect();
_ro = null;
}
,
observe: (target) => {
var _a;
return (_a = get()) == null ? void 0 : _a.observe(target, {
box: "border-box"
});
}
,
unobserve: (target) => {
var _a;
return (_a = get()) == null ? void 0 : _a.unobserve(target);
}
};
}
)();
this.range = null;
this.setOptions = (opts2) => {
Object.entries(opts2).forEach( ([key,value]) => {
if (typeof value === "undefined")
delete opts2[key];
}
);
this.options = {
debug: false,
initialOffset: 0,
overscan: 1,
paddingStart: 0,
paddingEnd: 0,
scrollPaddingStart: 0,
scrollPaddingEnd: 0,
horizontal: false,
getItemKey: defaultKeyExtractor,
rangeExtractor: defaultRangeExtractor,
onChange: () => {}
,
measureElement,
initialRect: {
width: 0,
height: 0
},
scrollMargin: 0,
gap: 0,
indexAttribute: "data-index",
initialMeasurementsCache: [],
lanes: 1,
isScrollingResetDelay: 150,
enabled: true,
isRtl: false,
useScrollendEvent: false,
useAnimationFrameWithResizeObserver: false,
...opts2
};
}
;
this.notify = (sync) => {
var _a, _b;
(_b = (_a = this.options).onChange) == null ? void 0 : _b.call(_a, this, sync);
}
;
this.maybeNotify = (0,
_utils_js__WEBPACK_IMPORTED_MODULE_0__.memo)( () => {
this.calculateRange();
return [this.isScrolling, this.range ? this.range.startIndex : null, this.range ? this.range.endIndex : null];
}
, (isScrolling) => {
this.notify(isScrolling);
}
, {
key: true && "maybeNotify",
debug: () => this.options.debug,
initialDeps: [this.isScrolling, this.range ? this.range.startIndex : null, this.range ? this.range.endIndex : null]
});
this.cleanup = () => {
this.unsubs.filter(Boolean).forEach( (d) => d());
this.unsubs = [];
this.observer.disconnect();
this.scrollElement = null;
this.targetWindow = null;
}
;
this._didMount = () => {
return () => {
this.cleanup();
}
;
}
;
this._willUpdate = () => {
var _a;
const scrollElement = this.options.enabled ? this.options.getScrollElement() : null;
if (this.scrollElement !== scrollElement) {
this.cleanup();
if (!scrollElement) {
this.maybeNotify();
return;
}
this.scrollElement = scrollElement;
if (this.scrollElement && "ownerDocument"in this.scrollElement) {
this.targetWindow = this.scrollElement.ownerDocument.defaultView;
} else {
this.targetWindow = ((_a = this.scrollElement) == null ? void 0 : _a.window) ?? null;
}
this.elementsCache.forEach( (cached) => {
this.observer.observe(cached);
}
);
this._scrollToOffset(this.getScrollOffset(), {
adjustments: void 0,
behavior: void 0
});
this.unsubs.push(this.options.observeElementRect(this, (rect) => {
this.scrollRect = rect;
this.maybeNotify();
}
));
this.unsubs.push(this.options.observeElementOffset(this, (offset, isScrolling) => {
this.scrollAdjustments = 0;
this.scrollDirection = isScrolling ? this.getScrollOffset() < offset ? "forward" : "backward" : null;
this.scrollOffset = offset;
this.isScrolling = isScrolling;
this.maybeNotify();
}
));
}
}
;
this.getSize = () => {
if (!this.options.enabled) {
this.scrollRect = null;
return 0;
}
this.scrollRect = this.scrollRect ?? this.options.initialRect;
return this.scrollRect[this.options.horizontal ? "width" : "height"];
}
;
this.getScrollOffset = () => {
if (!this.options.enabled) {
this.scrollOffset = null;
return 0;
}
this.scrollOffset = this.scrollOffset ?? (typeof this.options.initialOffset === "function" ? this.options.initialOffset() : this.options.initialOffset);
return this.scrollOffset;
}
;
this.getFurthestMeasurement = (measurements, index) => {
const furthestMeasurementsFound = /* @__PURE__ */
new Map();
const furthestMeasurements = /* @__PURE__ */
new Map();
for (let m = index - 1; m >= 0; m--) {
const measurement = measurements[m];
if (furthestMeasurementsFound.has(measurement.lane)) {
continue;
}
const previousFurthestMeasurement = furthestMeasurements.get(measurement.lane);
if (previousFurthestMeasurement == null || measurement.end > previousFurthestMeasurement.end) {
furthestMeasurements.set(measurement.lane, measurement);
} else if (measurement.end < previousFurthestMeasurement.end) {
furthestMeasurementsFound.set(measurement.lane, true);
}
if (furthestMeasurementsFound.size === this.options.lanes) {
break;
}
}
return furthestMeasurements.size === this.options.lanes ? Array.from(furthestMeasurements.values()).sort( (a, b) => {
if (a.end === b.end) {
return a.index - b.index;
}
return a.end - b.end;
}
)[0] : void 0;
}
;
this.getMeasurementOptions = (0,
_utils_js__WEBPACK_IMPORTED_MODULE_0__.memo)( () => [this.options.count, this.options.paddingStart, this.options.scrollMargin, this.options.getItemKey, this.options.enabled], (count, paddingStart, scrollMargin, getItemKey, enabled) => {
this.pendingMeasuredCacheIndexes = [];
return {
count,
paddingStart,
scrollMargin,
getItemKey,
enabled
};
}
, {
key: false
});
this.getMeasurements = (0,
_utils_js__WEBPACK_IMPORTED_MODULE_0__.memo)( () => [this.getMeasurementOptions(), this.itemSizeCache], ({count, paddingStart, scrollMargin, getItemKey, enabled}, itemSizeCache) => {
if (!enabled) {
this.measurementsCache = [];
this.itemSizeCache.clear();
return [];
}
if (this.measurementsCache.length === 0) {
this.measurementsCache = this.options.initialMeasurementsCache;
this.measurementsCache.forEach( (item) => {
this.itemSizeCache.set(item.key, item.size);
}
);
}
const min = this.pendingMeasuredCacheIndexes.length > 0 ? Math.min(...this.pendingMeasuredCacheIndexes) : 0;
this.pendingMeasuredCacheIndexes = [];
const measurements = this.measurementsCache.slice(0, min);
for (let i = min; i < count; i++) {
const key = getItemKey(i);
const furthestMeasurement = this.options.lanes === 1 ? measurements[i - 1] : this.getFurthestMeasurement(measurements, i);
const start = furthestMeasurement ? furthestMeasurement.end + this.options.gap : paddingStart + scrollMargin;
const measuredSize = itemSizeCache.get(key);
const size = typeof measuredSize === "number" ? measuredSize : this.options.estimateSize(i);
const end = start + size;
const lane = furthestMeasurement ? furthestMeasurement.lane : i % this.options.lanes;
measurements[i] = {
index: i,
start,
size,
end,
key,
lane
};
}
this.measurementsCache = measurements;
return measurements;
}
, {
key: true && "getMeasurements",
debug: () => this.options.debug
});
this.calculateRange = (0,
_utils_js__WEBPACK_IMPORTED_MODULE_0__.memo)( () => [this.getMeasurements(), this.getSize(), this.getScrollOffset(), this.options.lanes], (measurements, outerSize, scrollOffset, lanes) => {
return this.range = measurements.length > 0 && outerSize > 0 ? calculateRange({
measurements,
outerSize,
scrollOffset,
lanes
}) : null;
}
, {
key: true && "calculateRange",
debug: () => this.options.debug
});
this.getVirtualIndexes = (0,
_utils_js__WEBPACK_IMPORTED_MODULE_0__.memo)( () => {
let startIndex = null;
let endIndex = null;
const range = this.calculateRange();
if (range) {
startIndex = range.startIndex;
endIndex = range.endIndex;
}
this.maybeNotify.updateDeps([this.isScrolling, startIndex, endIndex]);
return [this.options.rangeExtractor, this.options.overscan, this.options.count, startIndex, endIndex];
}
, (rangeExtractor, overscan, count, startIndex, endIndex) => {
return startIndex === null || endIndex === null ? [] : rangeExtractor({
startIndex,
endIndex,
overscan,
count
});
}
, {
key: true && "getVirtualIndexes",
debug: () => this.options.debug
});
this.indexFromElement = (node) => {
const attributeName = this.options.indexAttribute;
const indexStr = node.getAttribute(attributeName);
if (!indexStr) {
console.warn(`Missing attribute name '${attributeName}={index}' on measured element.`);
return -1;
}
return parseInt(indexStr, 10);
}
;
this._measureElement = (node, entry) => {
const index = this.indexFromElement(node);
const item = this.measurementsCache[index];
if (!item) {
return;
}
const key = item.key;
const prevNode = this.elementsCache.get(key);
if (prevNode !== node) {
if (prevNode) {
this.observer.unobserve(prevNode);
}
this.observer.observe(node);
this.elementsCache.set(key, node);
}
if (node.isConnected) {
this.resizeItem(index, this.options.measureElement(node, entry, this));
}
}
;
this.resizeItem = (index, size) => {
const item = this.measurementsCache[index];
if (!item) {
return;
}
const itemSize = this.itemSizeCache.get(item.key) ?? item.size;
const delta = size - itemSize;
if (delta !== 0) {
if (this.shouldAdjustScrollPositionOnItemSizeChange !== void 0 ? this.shouldAdjustScrollPositionOnItemSizeChange(item, delta, this) : item.start < this.getScrollOffset() + this.scrollAdjustments) {
if (true && this.options.debug) {
console.info("correction", delta);
}
this._scrollToOffset(this.getScrollOffset(), {
adjustments: this.scrollAdjustments += delta,
behavior: void 0
});
}
this.pendingMeasuredCacheIndexes.push(item.index);
this.itemSizeCache = new Map(this.itemSizeCache.set(item.key, size));
this.notify(false);
}
}
;
this.measureElement = (node) => {
if (!node) {
this.elementsCache.forEach( (cached, key) => {
if (!cached.isConnected) {
this.observer.unobserve(cached);
this.elementsCache.delete(key);
}
}
);
return;
}
this._measureElement(node, void 0);
}
;
this.getVirtualItems = (0,
_utils_js__WEBPACK_IMPORTED_MODULE_0__.memo)( () => [this.getVirtualIndexes(), this.getMeasurements()], (indexes, measurements) => {
const virtualItems = [];
for (let k = 0, len = indexes.length; k < len; k++) {
const i = indexes[k];
const measurement = measurements[i];
virtualItems.push(measurement);
}
return virtualItems;
}
, {
key: true && "getVirtualItems",
debug: () => this.options.debug
});
this.getVirtualItemForOffset = (offset) => {
const measurements = this.getMeasurements();
if (measurements.length === 0) {
return void 0;
}
return (0,
_utils_js__WEBPACK_IMPORTED_MODULE_0__.notUndefined)(measurements[findNearestBinarySearch(0, measurements.length - 1, (index) => (0,
_utils_js__WEBPACK_IMPORTED_MODULE_0__.notUndefined)(measurements[index]).start, offset)]);
}
;
this.getOffsetForAlignment = (toOffset, align, itemSize=0) => {
const size = this.getSize();
const scrollOffset = this.getScrollOffset();
if (align === "auto") {
align = toOffset >= scrollOffset + size ? "end" : "start";
}
if (align === "center") {
toOffset += (itemSize - size) / 2;
} else if (align === "end") {
toOffset -= size;
}
const maxOffset = this.getTotalSize() + this.options.scrollMargin - size;
return Math.max(Math.min(maxOffset, toOffset), 0);
}
;
this.getOffsetForIndex = (index, align="auto") => {
index = Math.max(0, Math.min(index, this.options.count - 1));
const item = this.measurementsCache[index];
if (!item) {
return void 0;
}
const size = this.getSize();
const scrollOffset = this.getScrollOffset();
if (align === "auto") {
if (item.end >= scrollOffset + size - this.options.scrollPaddingEnd) {
align = "end";
} else if (item.start <= scrollOffset + this.options.scrollPaddingStart) {
align = "start";
} else {
return [scrollOffset, align];
}
}
const toOffset = align === "end" ? item.end + this.options.scrollPaddingEnd : item.start - this.options.scrollPaddingStart;
return [this.getOffsetForAlignment(toOffset, align, item.size), align];
}
;
this.isDynamicMode = () => this.elementsCache.size > 0;
this.scrollToOffset = (toOffset, {align="start", behavior}={}) => {
if (behavior === "smooth" && this.isDynamicMode()) {
console.warn("The `smooth` scroll behavior is not fully supported with dynamic size.");
}
this._scrollToOffset(this.getOffsetForAlignment(toOffset, align), {
adjustments: void 0,
behavior
});
}
;
this.scrollToIndex = (index, {align: initialAlign="auto", behavior}={}) => {
if (behavior === "smooth" && this.isDynamicMode()) {
console.warn("The `smooth` scroll behavior is not fully supported with dynamic size.");
}
index = Math.max(0, Math.min(index, this.options.count - 1));
let attempts = 0;
const maxAttempts = 10;
const tryScroll = (currentAlign) => {
if (!this.targetWindow)
return;
const offsetInfo = this.getOffsetForIndex(index, currentAlign);
if (!offsetInfo) {
console.warn("Failed to get offset for index:", index);
return;
}
const [offset,align] = offsetInfo;
this._scrollToOffset(offset, {
adjustments: void 0,
behavior
});
this.targetWindow.requestAnimationFrame( () => {
const currentOffset = this.getScrollOffset();
const afterInfo = this.getOffsetForIndex(index, align);
if (!afterInfo) {
console.warn("Failed to get offset for index:", index);
return;
}
if (!(0,
_utils_js__WEBPACK_IMPORTED_MODULE_0__.approxEqual)(afterInfo[0], currentOffset)) {
scheduleRetry(align);
}
}
);
}
;
const scheduleRetry = (align) => {
if (!this.targetWindow)
return;
attempts++;
if (attempts < maxAttempts) {
if (true && this.options.debug) {
console.info("Schedule retry", attempts, maxAttempts);
}
this.targetWindow.requestAnimationFrame( () => tryScroll(align));
} else {
console.warn(`Failed to scroll to index ${index} after ${maxAttempts} attempts.`);
}
}
;
tryScroll(initialAlign);
}
;
this.scrollBy = (delta, {behavior}={}) => {
if (behavior === "smooth" && this.isDynamicMode()) {
console.warn("The `smooth` scroll behavior is not fully supported with dynamic size.");
}
this._scrollToOffset(this.getScrollOffset() + delta, {
adjustments: void 0,
behavior
});
}
;
this.getTotalSize = () => {
var _a;
const measurements = this.getMeasurements();
let end;
if (measurements.length === 0) {
end = this.options.paddingStart;
} else if (this.options.lanes === 1) {
end = ((_a = measurements[measurements.length - 1]) == null ? void 0 : _a.end) ?? 0;
} else {
const endByLane = Array(this.options.lanes).fill(null);
let endIndex = measurements.length - 1;
while (endIndex >= 0 && endByLane.some( (val) => val === null)) {
const item = measurements[endIndex];
if (endByLane[item.lane] === null) {
endByLane[item.lane] = item.end;
}
endIndex--;
}
end = Math.max(...endByLane.filter( (val) => val !== null));
}
return Math.max(end - this.options.scrollMargin + this.options.paddingEnd, 0);
}
;
this._scrollToOffset = (offset, {adjustments, behavior}) => {
this.options.scrollToFn(offset, {
behavior,
adjustments
}, this);
}
;
this.measure = () => {
this.itemSizeCache = /* @__PURE__ */
new Map();
this.notify(false);
}
;
this.setOptions(opts);
}
}
const findNearestBinarySearch = (low, high, getCurrentValue, value) => {
while (low <= high) {
const middle = (low + high) / 2 | 0;
const currentValue = getCurrentValue(middle);
if (currentValue < value) {
low = middle + 1;
} else if (currentValue > value) {
high = middle - 1;
} else {
return middle;
}
}
if (low > 0) {
return low - 1;
} else {
return 0;
}
}
;
function calculateRange({measurements, outerSize, scrollOffset, lanes}) {
const lastIndex = measurements.length - 1;
const getOffset = (index) => measurements[index].start;
if (measurements.length <= lanes) {
return {
startIndex: 0,
endIndex: lastIndex
};
}
let startIndex = findNearestBinarySearch(0, lastIndex, getOffset, scrollOffset);
let endIndex = startIndex;
if (lanes === 1) {
while (endIndex < lastIndex && measurements[endIndex].end < scrollOffset + outerSize) {
endIndex++;
}
} else if (lanes > 1) {
const endPerLane = Array(lanes).fill(0);
while (endIndex < lastIndex && endPerLane.some( (pos) => pos < scrollOffset + outerSize)) {
const item = measurements[endIndex];
endPerLane[item.lane] = item.end;
endIndex++;
}
const startPerLane = Array(lanes).fill(scrollOffset + outerSize);
while (startIndex >= 0 && startPerLane.some( (pos) => pos >= scrollOffset)) {
const item = measurements[startIndex];
startPerLane[item.lane] = item.start;
startIndex--;
}
startIndex = Math.max(0, startIndex - startIndex % lanes);
endIndex = Math.min(lastIndex, endIndex + (lanes - 1 - endIndex % lanes));
}
return {
startIndex,
endIndex
};
}
Your minimal, reproducible example
Note: I did not provide an onChange prop to the Virtualizer. The infinite update loop is triggered when the outer container’s dimensions change (for example, when resizing the browser window or the parent element).
Steps to reproduce
Note: I did not provide an onChange prop to the Virtualizer.
The infinite update loop is triggered when the outer container’s dimensions change (for example, when resizing the browser window or the parent element).
Expected behavior
list
How often does this bug happen?
None
Screenshots or Videos
No response
Platform
web
tanstack-virtual version
latest
TypeScript version
No response
Additional context
No response
Terms & Code of Conduct
- I agree to follow this project's Code of Conduct
- I understand that if my bug cannot be reliable reproduced in a debuggable environment, it will probably not be fixed and this issue may even be closed.
- 主要语言
- TypeScript
- 星标
- 7.1k
- 派生
- 472
- 平均合并
- 4 天 8 小时
- 30 天内合并 PR
- 14
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
TanStack/virtual 的其他 Issue
-
anchorTo: 'end' loses the end when the scroll element shrinks可能已有人在做 @langleyd 于 2 天前认领。 未关闭
难度 3/5 1-2 天 新手友好度 68/100
维护者通常 2 天内回复
-
virtual-core: desktop Safari drops the prepend anchor write when history lands during the top rubber-band bounce可能已有人在做 关联的 PR 仍在进行中或已合并。 未关闭
难度 5/5 一周以上 新手友好度 38/100
维护者通常 2 天内回复
-
marko-virtual: scrollToEnd during an in-flight prepend strands the view one prepend above the bottom (chat-pretext e2e fails on CI)可能重新可做 关联的 PR 已关闭且未合并。 未关闭
难度 4/5 3-5 天 新手友好度 55/100
维护者通常 2 天内回复
-
难度 4/5 3-5 天 新手友好度 42/100
TanStack/virtual#1252 · 1 条评论 ·
维护者通常 2 天内回复
-
lit-virtual: VirtualizerController never applies updated options after construction可能已有人在做 @waterWang 于 59 天前认领。 未关闭
难度 5/5 一周以上 新手友好度 45/100
维护者通常 2 天内回复
相似的 Issue
-
难度 2/5 1-3 小时 新手友好度 66/100
cockpit-project/cockpit-machines#2835 ·
维护者通常 2 天内回复
-
难度 2/5 1-3 小时 新手友好度 72/100
cloudflare/kumo#866 ·
维护者通常 1 天内回复
-
area:connector bug
难度 1/5 1 小时以内 新手友好度 82/100
维护者通常 1 天内回复
-
environment: OPENCODE_API_KEY is described as a Console service account key, but a Go subscription key works可能已有人在做 关联的 PR 仍在进行中或已合并。 未关闭
难度 2/5 1-3 小时 新手友好度 78/100
tester-army/e2e#1015 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 62/100
维护者通常 1 天内回复