🐛 Bug Report: “Maximum update depth exceeded” when using onChange with Virtualizer
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 45/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Cần làm rõ
- Mức độ hoạt động
- Ít trao đổi
- Công nghệ
- react, typescript
- Lĩnh vực
- frontend, performance
Hướng nghiên cứu
Bắt đầu với cấu hình useVirtualizer được báo cáo và stack gọi qua Virtualizer.notify, resizeItem và _measureElement; tái hiện lỗi bằng cách thay đổi kích thước của container bên ngoài. Báo cáo không nêu tệp mã nguồn hay test nào trong repository, vì vậy trước tiên hãy xác định implementation của React virtualizer và các test resize hiện có. Hoàn tất khi việc resize không tạo ra vòng lặp maximum-update-depth và hành vi virtualization thông thường vẫn nguyên vẹn.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
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.
- Ngôn ngữ chính
- TypeScript
- Star
- 7.1k
- Fork
- 472
- Merge trung bình
- 1 ngày 2 giờ
- Pull request đã merge (30 ngày)
- 12
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của TanStack/virtual
-
measureElement caches 0 size for nodes that aren't attached yet (Vue: rows stack at start 0 and the list scrolls away on mount)Có thể đã có người làm @webn00b đã nhận 10 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
Maintainer thường phản hồi trong vòng 1 ngày
-
`scrollToIndex(last, { align: 'end' })` stays short of the end after a measured row grows (virtual-core ≥ 3.17.0)Có thể đã có người làm @minwookshin đã nhận 7 ngày trước. Đang mở
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 55/100
TanStack/virtual#1290 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
virtual-core: desktop Safari drops the prepend anchor write when history lands during the top rubber-band bounceCó thể đã có người làm Có pull request liên kết đang mở hoặc đã được merge. Đang mở
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 38/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 62/100
Maintainer thường phản hồi trong vòng 1 ngày
-
marko-virtual: scrollToEnd during an in-flight prepend strands the view one prepend above the bottom (chat-pretext e2e fails on CI)Có thể làm lại được Pull request cho issue này đã bị đóng mà không được merge. Đang mở
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 55/100
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của TanStack/virtual
Issue tương tự
-
fix: txId branch of the contract-state streams drops every state after the named transactionĐang mởbot:ai-assisted status:untriaged
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 85/100
midnightntwrk/midnight-js#1424 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
enhancement
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
mksglu/context-mode#1268 ·
Maintainer thường phản hồi trong vòng 5 ngày
-
[bug] Setup fails with "Cannot find matching keyid" when an older Node's corepack is on PATHCó thể đã có người làm @EyalPoly đã nhận hôm nay. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
MystenLabs/MemWal#1124 · 2 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Edit:Đang mởcheck:failed streams:edit
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 60/100
iptv-org/iptv#54352 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày