diff --git a/CHANGELOG.md b/CHANGELOG.md index 976268f..1230de9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ +1.11.3 / 05.02.2023 +================== + +* Renamed `onItemHeightChange()` to `onItemHeightDidChange()`. + 1.11.0 / 19.01.2023 ================== diff --git a/README.md b/README.md index 74e6cfc..276b667 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ The following `state` properties are only used for saving and restoring `Virtual * `itemStates: any[]` — The states of all items. Any change in an item's appearance while it's rendered must be reflected in changing that item's state by calling `.setItemState(i, itemState)` instance method (described below): this way, the item's state is preserved when it's shown next time after being hidden due to going off screen. For example, if an item is a social media comment, and there's a "Show more"/"Show less" button that shows the full text of the comment, then it must call `.setItemState(i, { showMore: true/false })` every time. -* `itemHeights: number[]` — The measured heights of all items. If an item's height hasn't been measured yet then it's `undefined`. By default, items are only measured once: when they're initially rendered. If an item's height changes afterwards, then `.onItemHeightChange(i)` instance method must be called (described below), otherwise `VirtualScroller`'s calculations will be off. For example, if an item is a social media comment, and there's a "Show more"/"Show less" button that shows the full text of the comment, then it must call `.onItemHeightChange(i)` every time. And every change in an item's height must come as a result of changing some kind of state, be it the item's state in `VirtualScroller` via `.setItemState()`, or some other state managed by the application. +* `itemHeights: number[]` — The measured heights of all items. If an item's height hasn't been measured yet then it's `undefined`. By default, items are only measured once: when they're initially rendered. If an item's height changes afterwards, then `.onItemHeightDidChange(i)` instance method must be called right after it happens (described later in the document), otherwise `VirtualScroller`'s calculations will be off. For example, if an item is a social media comment, and there's a "Show more"/"Show less" button that shows the full text of the comment, then it must call `.onItemHeightDidChange(i)` every time the comment text has been expanded or collapsed. And every change in an item's height must conceptually be a result of changing some kind of "state", be it the item's state in `VirtualScroller` updated via `.setItemState()`, or some other "state" that got updated by the application. * `verticalSpacing: number?` — Vertical item spacing. Is `undefined` until it has been measured. Is only measured once, when at least two rows of items have been rendered. @@ -343,19 +343,19 @@ When using custom (external) state management, contrary to the default (internal #### "Advanced" (rarely used) instance methods -* `onItemHeightChange(i: number)` — (advanced) If an item's height could change, this function should be called immediately after the item's height has changed. An example would be having an "Expand"/"Collapse" button in a list item. The function re-measures the item's height and re-calculates `VirtualScroller` layout. +* `onItemHeightDidChange(i: number)` — (advanced) If an item's height could've changed, this function should be called immediately after the item's height has potentially changed. An example would be having an "Expand"/"Collapse" button in a list item. The function re-measures the item's height and re-calculates `VirtualScroller` layout. * There's also a convention that every change in an item's height must come as a result of changing its "state", be it the item's state that is stored internally in the `VirtualScroller` state (see `.setItemState()` function) or some other application-level state that is stored outside of the `VirtualScroller` state. - * Implementation-wise, calling `onItemHeightChange()` manually could be replaced with detecting item height changes automatically via [Resize Observer](https://caniuse.com/#search=Resize%20Observer) in some future version. + * Implementation-wise, calling `onItemHeightDidChange()` manually could be replaced with detecting item height changes automatically via [Resize Observer](https://caniuse.com/#search=Resize%20Observer) in some future version. * `setItemState(i: number, itemState: any?)` — (advanced) Replaces a list item's state inside `VirtualScroller` state. Use it to preserve an item's state because offscreen items get unmounted and any unsaved state is lost in the process. If an item's state is correctly preserved, it will be restored when an item gets mounted again due to becoming visible. - * Calling `setItemState()` doesn't trigger a re-layout of `VirtualScroller` because changing a list item's state doesn't necessarily mean a change of its height, so a re-layout might not be required. If an item's height did change as a result of changing its state, then `VirtualScroller` layout must be updated, and to do that, call `onItemHeightChange(i)` after calling `setItemState()` has taken effect. + * Calling `setItemState()` doesn't trigger a re-layout of `VirtualScroller` because changing a list item's state doesn't necessarily mean a change of its height, so a re-layout might not be required. If an item's height did change as a result of changing its state, then `VirtualScroller` layout must be updated, and to do that, call `onItemHeightDidChange(i)` right after calling `setItemState()` has taken effect. * For example, consider a social network feed, where each post optionally has an attachment. Suppose there's a post in the feed having a YouTube video attachment. The attachment is initially shown as a small thumbnail that expands into a full-sized embedded YouTube video player when a user clicks on it. If the expanded/collapsed state of such attachment wasn't stored in `VirtualScroller` state then the following scenario would be possible: the user expands the video, then scrolls down so that the post with the video is no longer visible, the post gets unmounted due to going off screen, then the user scrolls back up so that the post with the video is visible again, the post gets mounted, but the video is not expanded and instead of it a small thumbnail is shown because there's no previous "state" to restore. - * In the example above, one should also call `onItemHeightChange(i)` after the YouTube video has been expanded/collapsed. Otherwise, the scroll position would "jump" when the item goes off screen, because `VirtualScroller` would have based its calculations on the initially measured item height, not the "expanded" one, so it would subtract an incorrect value from the list's top margin, resulting in a "jump of content". + * In the example above, one should also call `onItemHeightDidChange(i)` right after the YouTube video has been expanded/collapsed. Otherwise, the scroll position would "jump" when the item goes off screen, because `VirtualScroller` would have based its calculations on the initially measured item height, not the "expanded" one, so it would subtract an incorrect value from the list's top margin, resulting in a "jump of content". * `getItemScrollPosition(i: number): number?` — (advanced) Returns an item's scroll position inside the scrollable container. Returns `undefined` if any of the items before this item haven't been rendered yet. @@ -446,7 +446,7 @@ const virtualScroller = new VirtualScroller( * `setItems(items, options)` — A proxy for the corresponding `VirtualScroller` method. -* `onItemHeightChange(i)` — A proxy for the corresponding `VirtualScroller` method. +* `onItemHeightDidChange(i)` — A proxy for the corresponding `VirtualScroller` method. * `setItemState(i, itemState)` — A proxy for the corresponding `VirtualScroller` method. @@ -467,7 +467,7 @@ The required properties are: * `item: any` — The item object itself (an element of the `items` array). * `state: any?` — Item's state. See the description of `itemStates` property of `VirtualScroller` `state` for more details. * `setState(newState: any?)` — Can be called to replace item's state. See the description of `setItemState(i, newState)` function of `VirtualScroller` for more details. - * `onHeightChange(i)` — If an item's height could change, this function should be called immediately after the item's height has changed. See the description of `onItemHeightChange()` function of `VirtualScroller` for more details. + * `onHeightDidChange(i)` — If an item's height could change after the initial render, this function should be called immediately after the item's height has potentially changed. See the description of `onItemHeightDidChange()` function of `VirtualScroller` for more details. * For best performance, make sure that `itemComponent` is a `React.memo()` component or a `React.PureComponent`. Otherwise, list items will keep re-rendering themselves as the user scrolls because the containing `` component gets re-rendered on scroll. @@ -550,7 +550,7 @@ To fix that, `itemComponent` receives the following state management properties: * In the example described above, `onHeightChange()` would be called immediately after a user has clicked a "Show more"/"Show less" button and the component has re-rendered itself, because that results in a change of the item element's height, so `VirtualScroller` should re-measure it in order for its internal calculations to stay correct. - * This is simply a proxy for `virtualScroller.onItemHeightChange(i)`. + * This is simply a proxy for `virtualScroller.onItemHeightDidChange(i)`. ```js function ItemComponent({ @@ -1008,7 +1008,7 @@ One can use any npm CDN service, e.g. [unpkg.com](https://unpkg.com) or [jsdeliv diff --git a/dom/index.d.ts b/dom/index.d.ts index 999b332..0f4b6e8 100644 --- a/dom/index.d.ts +++ b/dom/index.d.ts @@ -21,6 +21,6 @@ export default class VirtualScroller { // start(): void; stop(): void; setItems(newItems: Item[], options?: SetItemsOptions): void; - onItemHeightChange(i: number): void; + onItemHeightDidChange(i: number): void; setItemState(i: number, newState: ItemState): void; } \ No newline at end of file diff --git a/index.d.ts b/index.d.ts index c3efe31..ba631de 100644 --- a/index.d.ts +++ b/index.d.ts @@ -93,7 +93,7 @@ export default class VirtualScroller { updateLayout(): void; onRender(): void; setItems(newItems: Item[], options?: SetItemsOptions): void; - onItemHeightChange(i: number): void; + onItemHeightDidChange(i: number): void; setItemState(i: number, itemState?: object): void; getItemScrollPosition(i: number): number | undefined; getInitialState(): State; diff --git a/package-lock.json b/package-lock.json index 4adc9ab..6179ca7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "virtual-scroller", - "version": "1.11.2", + "version": "1.11.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 3322898..6050298 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "virtual-scroller", - "version": "1.11.2", + "version": "1.11.3", "description": "A component for efficiently rendering large lists of variable height items", "main": "index.cjs", "module": "index.js", diff --git a/source/DOM/VirtualScroller.js b/source/DOM/VirtualScroller.js index b1d21a1..60d3783 100644 --- a/source/DOM/VirtualScroller.js +++ b/source/DOM/VirtualScroller.js @@ -141,8 +141,17 @@ export default class VirtualScroller { } } + /** + * @deprecated + * `.onItemHeightChange()` has been renamed to `.onItemHeightDidChange()`. + */ onItemHeightChange(i) { - this.virtualScroller.onItemHeightChange(i) + warn('`.onItemHeightChange(i)` method was renamed to `.onItemHeightDidChange(i)`') + this.onItemHeightDidChange(i) + } + + onItemHeightDidChange(i) { + this.virtualScroller.onItemHeightDidChange(i) } setItemState(i, newState) { @@ -154,6 +163,7 @@ export default class VirtualScroller { * `.updateItems()` has been renamed to `.setItems()`. */ updateItems(newItems, options) { + warn('`.updateItems()` method was renamed to `.setItems(i)`') this.setItems(newItems, options) } diff --git a/source/ItemHeights.js b/source/ItemHeights.js index 8aa4030..182a53b 100644 --- a/source/ItemHeights.js +++ b/source/ItemHeights.js @@ -119,7 +119,7 @@ export default class ItemHeights { // Measure item heights that haven't been measured previously. // Don't re-measure item heights that have been measured previously. // The rationale is that developers are supposed to manually call - // `.onItemHeightChange()` every time an item's height changes. + // `.onItemHeightDidChange()` immediately every time an item's height has changed. // If developers don't neglect that rule, item heights won't // change unexpectedly. if (this._get(i) === undefined) { @@ -169,7 +169,7 @@ export default class ItemHeights { const previousHeight = this._get(i) const height = this._measureItemHeight(i, firstShownItemIndex) if (previousHeight !== height) { - warn('Item index', i, 'height changed unexpectedly: it was', previousHeight, 'before, but now it is', height, '. An item\'s height is allowed to change only in two cases: when the item\'s "state" changes and the developer calls `setItemState(i, newState)`, or when the item\'s height changes for any reason and the developer calls `onItemHeightChange(i)`. Perhaps you forgot to persist the item\'s "state" by calling `setItemState(i, newState)` when it changed, and that "state" got lost when the item element was unmounted, which resulted in a different height when the item was shown again having its "state" reset.') + warn('Item index', i, 'height changed unexpectedly: it was', previousHeight, 'before, but now it is', height, '. An item\'s height is allowed to change only in two cases: when the item\'s "state" changes and the developer calls `setItemState(i, newState)`, or when the item\'s height changes for any reason and the developer calls `onItemHeightDidChange(i)` right after that happens. Perhaps you forgot to persist the item\'s "state" by calling `setItemState(i, newState)` when it changed, and that "state" got lost when the item element was unmounted, which resulted in a different height when the item was shown again having its "state" reset.') // Update the item's height as an attempt to fix things. this._set(i, height) } @@ -189,18 +189,18 @@ export default class ItemHeights { remeasureItemHeight(i, firstShownItemIndex) { const previousHeight = this._get(i) const height = this._measureItemHeight(i, firstShownItemIndex) - // // Because this function is called from `.onItemHeightChange()`, + // // Because this function is called from `.onItemHeightDidChange()`, // // there're no guarantees in which circumstances a developer calls it, // // and for which item indexes. // // Therefore, to guard against cases of incorrect usage, // // this function won't crash anything if the item isn't rendered // // or hasn't been previously rendered. // if (height !== undefined) { - // reportError(`"onItemHeightChange()" has been called for item ${i}, but that item isn't rendered.`) + // reportError(`"onItemHeightDidChange()" has been called for item ${i}, but that item isn't currently rendered.`) // return // } // if (previousHeight === undefined) { - // reportError(`"onItemHeightChange()" has been called for item ${i}, but that item hasn't been rendered before.`) + // reportError(`"onItemHeightDidChange()" has been called for item ${i}, but that item hasn't been rendered before.`) // return // } this._set(i, height) diff --git a/source/VirtualScroller.js b/source/VirtualScroller.js index c6daf41..2b26bf7 100644 --- a/source/VirtualScroller.js +++ b/source/VirtualScroller.js @@ -1,7 +1,7 @@ import VirtualScrollerConstructor from './VirtualScroller.constructor.js' import { hasTbodyStyles, addTbodyStyles } from './DOM/tbody.js' import { LAYOUT_REASON } from './Layout.js' -import log from './utility/debug.js' +import log, { warn } from './utility/debug.js' export default class VirtualScroller { /** @@ -205,13 +205,22 @@ export default class VirtualScroller { return this.getListTopOffsetInsideScrollableContainer() + itemTopOffsetInList } + /** + * @deprecated + * `.onItemHeightChange()` has been renamed to `.onItemHeightDidChange()`. + */ + onItemHeightChange(i) { + warn('`.onItemHeightChange(i)` method was renamed to `.onItemHeightDidChange(i)`') + this.onItemHeightDidChange(i) + } + /** * Forces a re-measure of an item's height. * @param {number} i — Item index */ - onItemHeightChange(i) { + onItemHeightDidChange(i) { this.hasToBeStarted() - this._onItemHeightChange(i) + this._onItemHeightDidChange(i) } /** diff --git a/source/VirtualScroller.layout.js b/source/VirtualScroller.layout.js index 36ebdf5..4bfd30a 100644 --- a/source/VirtualScroller.layout.js +++ b/source/VirtualScroller.layout.js @@ -99,7 +99,7 @@ export default function() { // or an "Expand YouTube video" button, which would result // in the actual height of the list item being different // from what has been initially measured in `this.itemHeights[i]`, - // if the developer didn't call `.setItemState(i, newState)` and `.onItemHeightChange(i)`. + // if the developer didn't call `.setItemState(i, newState)` and `.onItemHeightDidChange(i)`. if (!validateWillBeHiddenItemHeightsAreAccurate.call(this, firstShownItemIndex, lastShownItemIndex)) { log('~ Because some of the will-be-hidden item heights (listed above) have changed since they\'ve last been measured, redo layout. ~') // Redo layout, now with the correct item heights. @@ -172,9 +172,9 @@ export default function() { // Instead of using a `this.previouslyCalculatedLayout` instance variable, // this code could use `this.getState()` because it reflects what's currently on screen, // but there's a single edge case when it could go out of sync — - // updating item heights externally via `.onItemHeightChange(i)`. + // updating item heights externally via `.onItemHeightDidChange(i)`. // - // If, for example, an item height was updated externally via `.onItemHeightChange(i)` + // If, for example, an item height was updated externally via `.onItemHeightDidChange(i)` // then `this.getState().itemHeights` would get updated immediately but // `this.getState().beforeItemsHeight` or `this.getState().afterItemsHeight` // would still correspond to the previous item height, so those would be "stale". @@ -269,7 +269,7 @@ export default function() { * or an "Expand YouTube video" button, which would result * in the actual height of the list item being different * from what has been initially measured in `this.itemHeights[i]`, - * if the developer didn't call `.setItemState(i, newState)` and `.onItemHeightChange(i)`. + * if the developer didn't call `.setItemState(i, newState)` and `.onItemHeightDidChange(i)`. */ function validateWillBeHiddenItemHeightsAreAccurate(firstShownItemIndex, lastShownItemIndex) { let isValid = true @@ -281,26 +281,26 @@ export default function() { // The item will be hidden. Re-measure its height. // The rationale is that there could be a situation when an item's // height has changed, and the developer has properly added an - // `.onItemHeightChange(i)` call to notify `VirtualScroller` + // `.onItemHeightDidChange(i)` call to notify `VirtualScroller` // about that change, but at the same time that wouldn't work. // For example, suppose there's a list of several items on a page, // and those items are in "minimized" state (having height 100px). // Then, a user clicks an "Expand all items" button, and all items // in the list are expanded (expanded item height is gonna be 700px). - // `VirtualScroller` demands that `.onItemHeightChange(i)` is called + // `VirtualScroller` demands that `.onItemHeightDidChange(i)` is called // in such cases, and the developer has properly added the code to do that. // So, if there were 10 "minimized" items visible on a page, then there - // will be 10 individual `.onItemHeightChange(i)` calls. No issues so far. - // But, as the first `.onItemHeightChange(i)` call executes, it immediately + // will be 10 individual `.onItemHeightDidChange(i)` calls. No issues so far. + // But, as the first `.onItemHeightDidChange(i)` call executes, it immediately // ("synchronously") triggers a re-layout, and that re-layout finds out // that now, because the first item is big, it occupies most of the screen // space, and only the first 3 items are visible on screen instead of 10, // and so it leaves the first 3 items mounted and unmounts the rest 7. // Then, after `VirtualScroller` has rerendered, the code returns to - // where it was executing, and calls `.onItemHeightChange(i)` for the + // where it was executing, and calls `.onItemHeightDidChange(i)` for the // second item. It also triggers an immediate re-layout that finds out // that only the first 2 items are visible on screen, and it unmounts - // the third one too. After that, it calls `.onItemHeightChange(i)` + // the third one too. After that, it calls `.onItemHeightDidChange(i)` // for the third item, but that item is no longer rendered, so its height // can't be measured, and the same's for all the rest of the original 10 items. // So, even though the developer has written their code properly, the @@ -318,7 +318,7 @@ export default function() { updatePreviouslyCalculatedLayoutOnItemHeightChange.call(this, i, previouslyMeasuredItemHeight, actualItemHeight) } isValid = false - warn('Item index', i, 'is no longer visible and will be unmounted. Its height has changed from', previouslyMeasuredItemHeight, 'to', actualItemHeight, 'since it was last measured. This is not necessarily a bug, and could happen, for example, on screen width change, or when there\'re several `onItemHeightChange(i)` calls issued at the same time, and the first one triggers a re-layout before the rest of them have had a chance to be executed.') + warn('Item index', i, 'is no longer visible and will be unmounted. Its height has changed from', previouslyMeasuredItemHeight, 'to', actualItemHeight, 'since it was last measured. This is not necessarily a bug, and could happen, for example, on screen width change, or when there\'re several `onItemHeightDidChange(i)` calls issued at the same time, and the first one triggers a re-layout before the rest of them have had a chance to be executed.') } } i++ @@ -370,7 +370,7 @@ export default function() { return listTopOffset } - this._onItemHeightChange = (i) => { + this._onItemHeightDidChange = (i) => { log('~ Re-measure item height ~') log('Item index', i) @@ -383,36 +383,36 @@ export default function() { // Check if the item is still rendered. if (!(i >= firstShownItemIndex && i <= lastShownItemIndex)) { // There could be valid cases when an item is no longer rendered - // by the time `.onItemHeightChange(i)` gets called. + // by the time `.onItemHeightDidChange(i)` gets called. // For example, suppose there's a list of several items on a page, // and those items are in "minimized" state (having height 100px). // Then, a user clicks an "Expand all items" button, and all items // in the list are expanded (expanded item height is gonna be 700px). - // `VirtualScroller` demands that `.onItemHeightChange(i)` is called + // `VirtualScroller` demands that `.onItemHeightDidChange(i)` is called // in such cases, and the developer has properly added the code to do that. // So, if there were 10 "minimized" items visible on a page, then there - // will be 10 individual `.onItemHeightChange(i)` calls. No issues so far. - // But, as the first `.onItemHeightChange(i)` call executes, it immediately + // will be 10 individual `.onItemHeightDidChange(i)` calls. No issues so far. + // But, as the first `.onItemHeightDidChange(i)` call executes, it immediately // ("synchronously") triggers a re-layout, and that re-layout finds out // that now, because the first item is big, it occupies most of the screen // space, and only the first 3 items are visible on screen instead of 10, // and so it leaves the first 3 items mounted and unmounts the rest 7. // Then, after `VirtualScroller` has rerendered, the code returns to - // where it was executing, and calls `.onItemHeightChange(i)` for the + // where it was executing, and calls `.onItemHeightDidChange(i)` for the // second item. It also triggers an immediate re-layout that finds out // that only the first 2 items are visible on screen, and it unmounts - // the third one too. After that, it calls `.onItemHeightChange(i)` + // the third one too. After that, it calls `.onItemHeightDidChange(i)` // for the third item, but that item is no longer rendered, so its height // can't be measured, and the same's for all the rest of the original 10 items. // So, even though the developer has written their code properly, there're // still situations when the item could be no longer rendered by the time - // `.onItemHeightChange(i)` gets called. - return warn('The item is no longer rendered. This is not necessarily a bug, and could happen, for example, when when a developer calls `onItemHeightChange(i)` while looping through a batch of items.') + // `.onItemHeightDidChange(i)` gets called. + return warn('The item is no longer rendered. This is not necessarily a bug, and could happen, for example, when when a developer calls `onItemHeightDidChange(i)` while looping through a batch of items.') } const previousHeight = itemHeights[i] if (previousHeight === undefined) { - return reportError(`"onItemHeightChange()" has been called for item ${i}, but that item hasn't been rendered before.`) + return reportError(`"onItemHeightDidChange()" has been called for item ${i}, but that item hasn't been rendered before.`) } const newHeight = remeasureItemHeight.call(this, i) diff --git a/source/react/VirtualScroller.js b/source/react/VirtualScroller.js index 2d716fd..459421e 100644 --- a/source/react/VirtualScroller.js +++ b/source/react/VirtualScroller.js @@ -7,7 +7,7 @@ import useVirtualScrollerStartStop from './useVirtualScrollerStartStop.js' import useInstanceMethods from './useInstanceMethods.js' import useItemKeys from './useItemKeys.js' import useSetItemState from './useSetItemState.js' -import useOnItemHeightChange from './useOnItemHeightChange.js' +import useOnItemHeightDidChange from './useOnItemHeightDidChange.js' import useHandleItemsPropertyChange from './useHandleItemsPropertyChange.js' import useHandleItemIndexesChange from './useHandleItemIndexesChange.js' import useClassName from './useClassName.js' @@ -150,9 +150,9 @@ function VirtualScroller({ virtualScroller }) - // Cache per-item `onItemHeightChange` functions' "references" + // Cache per-item `onItemHeightDidChange` functions' "references" // so that item components don't get re-rendered needlessly. - const getOnItemHeightChange = useOnItemHeightChange({ + const getOnItemHeightDidChange = useOnItemHeightDidChange({ initialItemsCount: itemsProperty.length, virtualScroller }) @@ -251,7 +251,8 @@ function VirtualScroller({ state={itemStates && itemStates[i]} setState={getSetItemState(i)} onStateChange={getSetItemState(i)} - onHeightChange={getOnItemHeightChange(i)}> + onHeightChange={getOnItemHeightDidChange(i)} + onHeightDidChange={getOnItemHeightDidChange(i)}> {item} ) diff --git a/source/react/useOnItemHeightDidChange.js b/source/react/useOnItemHeightDidChange.js new file mode 100644 index 0000000..02b4b39 --- /dev/null +++ b/source/react/useOnItemHeightDidChange.js @@ -0,0 +1,28 @@ +import { useMemo, useRef, useCallback } from 'react' + +export default function useOnItemHeightDidChange({ + initialItemsCount, + virtualScroller +}) { + // Only compute the initial cache value once. + const initialCacheValue = useMemo(() => { + return new Array(initialItemsCount) + }, []) + + // Handler functions cache. + const cache = useRef(initialCacheValue) + + // Caches per-item `onItemHeightDidChange` functions' "references" + // so that item components don't get re-rendered needlessly. + const getOnItemHeightDidChange = useCallback((i) => { + if (!cache.current[i]) { + cache.current[i] = () => virtualScroller.onItemHeightDidChange(i) + } + return cache.current[i] + }, [ + virtualScroller, + cache + ]) + + return getOnItemHeightDidChange +} \ No newline at end of file