virtual-scroller 1.14.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +309 -250
  3. package/bundle/index-dom-bypass.html +198 -0
  4. package/bundle/index-dom-grid.html +204 -0
  5. package/bundle/index-dom-scrollableContainer.html +215 -0
  6. package/bundle/index-dom-tbody-scrollableContainer.html +81 -0
  7. package/bundle/index-dom-tbody.html +65 -0
  8. package/bundle/index-dom.html +115 -84
  9. package/bundle/{index-bypass.html → index-react-bypass.html} +83 -78
  10. package/bundle/{index-grid.html → index-react-grid.html} +78 -91
  11. package/bundle/{index-scrollableContainer.html → index-react-scrollableContainer.html} +96 -91
  12. package/bundle/index-react-strictMode.html +199 -0
  13. package/bundle/index-react-tbody-scrollableContainer.html +96 -0
  14. package/bundle/index-react-tbody.html +80 -0
  15. package/bundle/{messages.js → items.js} +1 -1
  16. package/bundle/lib/babel.min.js +25 -0
  17. package/bundle/lib/prop-types.min.js +1 -0
  18. package/bundle/lib/react-dom.development.js +29924 -0
  19. package/bundle/lib/react-dom.production.min.js +267 -0
  20. package/bundle/lib/react.development.js +3343 -0
  21. package/bundle/lib/react.production.min.js +31 -0
  22. package/bundle/style.base.css +33 -0
  23. package/{website/style.css → bundle/style.list.css} +10 -43
  24. package/bundle/style.list.grid.css +23 -0
  25. package/bundle/virtual-scroller-dom.js +1 -1
  26. package/bundle/virtual-scroller-dom.js.map +1 -1
  27. package/bundle/virtual-scroller-react.js +1 -1
  28. package/bundle/virtual-scroller-react.js.map +1 -1
  29. package/bundle/virtual-scroller.js +1 -1
  30. package/bundle/virtual-scroller.js.map +1 -1
  31. package/commonjs/BeforeResize.js +1 -2
  32. package/commonjs/BeforeResize.js.map +1 -1
  33. package/commonjs/DOM/VirtualScroller.js +7 -13
  34. package/commonjs/DOM/VirtualScroller.js.map +1 -1
  35. package/commonjs/DOM/tbody.js +6 -6
  36. package/commonjs/DOM/tbody.js.map +1 -1
  37. package/commonjs/ItemHeights.js +10 -13
  38. package/commonjs/ItemHeights.js.map +1 -1
  39. package/commonjs/Layout.defaults.js +21 -0
  40. package/commonjs/Layout.defaults.js.map +1 -0
  41. package/commonjs/Layout.js +75 -64
  42. package/commonjs/Layout.js.map +1 -1
  43. package/commonjs/Layout.test.js +8 -4
  44. package/commonjs/Layout.test.js.map +1 -1
  45. package/commonjs/VirtualScroller.constructor.js +38 -4
  46. package/commonjs/VirtualScroller.constructor.js.map +1 -1
  47. package/commonjs/VirtualScroller.items.js +50 -4
  48. package/commonjs/VirtualScroller.items.js.map +1 -1
  49. package/commonjs/VirtualScroller.js +23 -14
  50. package/commonjs/VirtualScroller.js.map +1 -1
  51. package/commonjs/VirtualScroller.layout.js +40 -29
  52. package/commonjs/VirtualScroller.layout.js.map +1 -1
  53. package/commonjs/VirtualScroller.onContainerResize.js +1 -2
  54. package/commonjs/VirtualScroller.onContainerResize.js.map +1 -1
  55. package/commonjs/VirtualScroller.state.js +10 -9
  56. package/commonjs/VirtualScroller.state.js.map +1 -1
  57. package/commonjs/VirtualScroller.verticalSpacing.js +39 -6
  58. package/commonjs/VirtualScroller.verticalSpacing.js.map +1 -1
  59. package/commonjs/react/VirtualScroller.js +85 -34
  60. package/commonjs/react/VirtualScroller.js.map +1 -1
  61. package/commonjs/react/useClassName.js +2 -2
  62. package/commonjs/react/useClassName.js.map +1 -1
  63. package/commonjs/react/useForwardedRef.js +50 -0
  64. package/commonjs/react/useForwardedRef.js.map +1 -0
  65. package/commonjs/react/useInstanceMethods.js +4 -4
  66. package/commonjs/react/useInstanceMethods.js.map +1 -1
  67. package/commonjs/react/useItemKeys.js +28 -5
  68. package/commonjs/react/useItemKeys.js.map +1 -1
  69. package/commonjs/react/useOnItemHeightDidChange.js +28 -12
  70. package/commonjs/react/useOnItemHeightDidChange.js.map +1 -1
  71. package/commonjs/react/useSetItemState.js +31 -12
  72. package/commonjs/react/useSetItemState.js.map +1 -1
  73. package/commonjs/react/useState.js +9 -9
  74. package/commonjs/react/useState.js.map +1 -1
  75. package/commonjs/react/{useStateNoStaleBug.js → useStateWithRepeatableRead.js} +3 -3
  76. package/commonjs/react/useStateWithRepeatableRead.js.map +1 -0
  77. package/commonjs/react/useStyle.js +10 -4
  78. package/commonjs/react/useStyle.js.map +1 -1
  79. package/commonjs/react/useValidateTableBodyItemsContainer.js +24 -0
  80. package/commonjs/react/useValidateTableBodyItemsContainer.js.map +1 -0
  81. package/commonjs/react/useVirtualScroller.js +4 -3
  82. package/commonjs/react/useVirtualScroller.js.map +1 -1
  83. package/commonjs/test/ItemsContainer.js +10 -10
  84. package/commonjs/test/ItemsContainer.js.map +1 -1
  85. package/commonjs/test/VirtualScroller.js +25 -10
  86. package/commonjs/test/VirtualScroller.js.map +1 -1
  87. package/dom/index.d.ts +6 -5
  88. package/index.d.ts +19 -8
  89. package/modules/BeforeResize.js +1 -2
  90. package/modules/BeforeResize.js.map +1 -1
  91. package/modules/DOM/VirtualScroller.js +7 -13
  92. package/modules/DOM/VirtualScroller.js.map +1 -1
  93. package/modules/DOM/tbody.js +4 -4
  94. package/modules/DOM/tbody.js.map +1 -1
  95. package/modules/ItemHeights.js +11 -14
  96. package/modules/ItemHeights.js.map +1 -1
  97. package/modules/Layout.defaults.js +11 -0
  98. package/modules/Layout.defaults.js.map +1 -0
  99. package/modules/Layout.js +74 -64
  100. package/modules/Layout.js.map +1 -1
  101. package/modules/Layout.test.js +8 -4
  102. package/modules/Layout.test.js.map +1 -1
  103. package/modules/VirtualScroller.constructor.js +37 -4
  104. package/modules/VirtualScroller.constructor.js.map +1 -1
  105. package/modules/VirtualScroller.items.js +51 -5
  106. package/modules/VirtualScroller.items.js.map +1 -1
  107. package/modules/VirtualScroller.js +23 -14
  108. package/modules/VirtualScroller.js.map +1 -1
  109. package/modules/VirtualScroller.layout.js +40 -29
  110. package/modules/VirtualScroller.layout.js.map +1 -1
  111. package/modules/VirtualScroller.onContainerResize.js +1 -2
  112. package/modules/VirtualScroller.onContainerResize.js.map +1 -1
  113. package/modules/VirtualScroller.state.js +10 -9
  114. package/modules/VirtualScroller.state.js.map +1 -1
  115. package/modules/VirtualScroller.verticalSpacing.js +38 -6
  116. package/modules/VirtualScroller.verticalSpacing.js.map +1 -1
  117. package/modules/react/VirtualScroller.js +84 -35
  118. package/modules/react/VirtualScroller.js.map +1 -1
  119. package/modules/react/useClassName.js +3 -3
  120. package/modules/react/useClassName.js.map +1 -1
  121. package/modules/react/useForwardedRef.js +42 -0
  122. package/modules/react/useForwardedRef.js.map +1 -0
  123. package/modules/react/useInstanceMethods.js +4 -4
  124. package/modules/react/useInstanceMethods.js.map +1 -1
  125. package/modules/react/useItemKeys.js +28 -5
  126. package/modules/react/useItemKeys.js.map +1 -1
  127. package/modules/react/useOnItemHeightDidChange.js +28 -12
  128. package/modules/react/useOnItemHeightDidChange.js.map +1 -1
  129. package/modules/react/useSetItemState.js +31 -12
  130. package/modules/react/useSetItemState.js.map +1 -1
  131. package/modules/react/useState.js +9 -9
  132. package/modules/react/useState.js.map +1 -1
  133. package/modules/react/{useStateNoStaleBug.js → useStateWithRepeatableRead.js} +2 -2
  134. package/modules/react/useStateWithRepeatableRead.js.map +1 -0
  135. package/modules/react/useStyle.js +10 -4
  136. package/modules/react/useStyle.js.map +1 -1
  137. package/modules/react/useValidateTableBodyItemsContainer.js +16 -0
  138. package/modules/react/useValidateTableBodyItemsContainer.js.map +1 -0
  139. package/modules/react/useVirtualScroller.js +4 -3
  140. package/modules/react/useVirtualScroller.js.map +1 -1
  141. package/modules/test/ItemsContainer.js +10 -10
  142. package/modules/test/ItemsContainer.js.map +1 -1
  143. package/modules/test/VirtualScroller.js +25 -10
  144. package/modules/test/VirtualScroller.js.map +1 -1
  145. package/package.json +1 -1
  146. package/react/as.d.ts +42 -0
  147. package/react/index.d.ts +204 -63
  148. package/source/BeforeResize.js +1 -2
  149. package/source/DOM/VirtualScroller.js +7 -13
  150. package/source/DOM/tbody.js +5 -5
  151. package/source/ItemHeights.js +11 -12
  152. package/source/Layout.defaults.js +8 -0
  153. package/source/Layout.js +66 -53
  154. package/source/Layout.test.js +7 -2
  155. package/source/VirtualScroller.constructor.js +27 -4
  156. package/source/VirtualScroller.items.js +47 -2
  157. package/source/VirtualScroller.js +23 -14
  158. package/source/VirtualScroller.layout.js +41 -28
  159. package/source/VirtualScroller.onContainerResize.js +1 -2
  160. package/source/VirtualScroller.state.js +10 -11
  161. package/source/VirtualScroller.verticalSpacing.js +32 -6
  162. package/source/react/VirtualScroller.js +96 -33
  163. package/source/react/useClassName.js +3 -3
  164. package/source/react/useForwardedRef.js +39 -0
  165. package/source/react/useInstanceMethods.js +12 -4
  166. package/source/react/useItemKeys.js +22 -4
  167. package/source/react/useOnItemHeightDidChange.js +29 -10
  168. package/source/react/useSetItemState.js +32 -10
  169. package/source/react/useState.js +6 -6
  170. package/source/react/{useStateNoStaleBug.js → useStateWithRepeatableRead.js} +1 -1
  171. package/source/react/useStyle.js +3 -2
  172. package/source/react/useValidateTableBodyItemsContainer.js +16 -0
  173. package/source/react/useVirtualScroller.js +4 -3
  174. package/source/test/ItemsContainer.js +10 -10
  175. package/source/test/VirtualScroller.js +16 -10
  176. package/website/index-dom-bypass.html +198 -0
  177. package/website/index-dom-grid.html +204 -0
  178. package/website/index-dom-scrollableContainer.html +215 -0
  179. package/website/index-dom-tbody-scrollableContainer.html +81 -0
  180. package/website/index-dom-tbody.html +65 -0
  181. package/website/index-dom.html +117 -84
  182. package/website/index-react-bypass.html +200 -0
  183. package/website/{index-grid.html → index-react-grid.html} +79 -92
  184. package/website/index-react-scrollableContainer.html +213 -0
  185. package/website/index-react-strictMode.html +199 -0
  186. package/website/index-react-tbody-scrollableContainer.html +96 -0
  187. package/website/index-react-tbody.html +80 -0
  188. package/website/{index-bypass.html → index-react.html} +84 -70
  189. package/website/index.html +84 -69
  190. package/website/{messages.js → items.js} +1 -1
  191. package/website/lib/babel.min.js +25 -0
  192. package/website/lib/prop-types.min.js +1 -0
  193. package/website/lib/react-dom.development.js +29924 -0
  194. package/website/lib/react-dom.production.min.js +267 -0
  195. package/website/lib/react.development.js +3343 -0
  196. package/website/lib/react.production.min.js +31 -0
  197. package/website/style.base.css +33 -0
  198. package/website/style.list.css +92 -0
  199. package/website/style.list.grid.css +23 -0
  200. package/bundle/index-tbody-scrollableContainer.html +0 -70
  201. package/bundle/index-tbody.html +0 -57
  202. package/bundle/on-scroll-to-dom.js +0 -2
  203. package/bundle/on-scroll-to-dom.js.map +0 -1
  204. package/bundle/on-scroll-to-react.js +0 -2
  205. package/bundle/on-scroll-to-react.js.map +0 -1
  206. package/bundle/on-scroll-to.js +0 -2
  207. package/bundle/on-scroll-to.js.map +0 -1
  208. package/commonjs/react/useStateNoStaleBug.js.map +0 -1
  209. package/modules/react/useStateNoStaleBug.js.map +0 -1
  210. package/website/index-scrollableContainer.html +0 -208
  211. package/website/index-tbody-scrollableContainer.html +0 -70
  212. package/website/index-tbody.html +0 -57
  213. package/website/lib/on-scroll-to-dom.js +0 -2
  214. package/website/lib/on-scroll-to-dom.js.map +0 -1
  215. package/website/lib/on-scroll-to-react.js +0 -2
  216. package/website/lib/on-scroll-to-react.js.map +0 -1
package/README.md CHANGED
@@ -6,24 +6,29 @@ A universal open-source implementation of Twitter's [`VirtualScroller`](https://
6
6
 
7
7
  * For React users, it exports a [React](#react) component from `virtual-scroller/react`.
8
8
  * For those who prefer "vanilla" DOM, it exports a [DOM](#dom) component from `virtual-scroller/dom`.
9
- * For everyone else, it exports a low-level [core](#core) component from `virtual-scroller`. The core component supports any type of UI "framework", or even any type of [rendering engine](#rendering-engine), not just DOM. Use it to create your own implementation for any UI "framework" or non-browser environment.
9
+ * For everyone else, it exports a ["core"](#core) component from `virtual-scroller`. The "core" component supports any type of UI "framework", or even any type of [rendering engine](#rendering-engine), not just DOM. Use it to create your own implementation for any UI "framework" or non-browser environment.
10
10
 
11
11
  ## Demo
12
12
 
13
13
  [DOM](#dom) component
14
14
 
15
15
  * [List](https://catamphetamine.gitlab.io/virtual-scroller/index-dom.html)
16
- * [Paginated List](https://catamphetamine.gitlab.io/virtual-scroller/index-dom.html?dynamic=✓)
17
- * [Table](https://catamphetamine.gitlab.io/virtual-scroller/index-tbody.html)
18
- * [Table in a scrollable container](https://catamphetamine.gitlab.io/virtual-scroller/index-tbody-scrollableContainer.html)
16
+ * [Paginated List](https://catamphetamine.gitlab.io/virtual-scroller/index-dom.html?pagination=✓)
17
+ * [List in a scrollable container](https://catamphetamine.gitlab.io/virtual-scroller/index-dom-scrollableContainer.html)
18
+ * [Table](https://catamphetamine.gitlab.io/virtual-scroller/index-dom-tbody.html)
19
+ * [Table in a scrollable container](https://catamphetamine.gitlab.io/virtual-scroller/index-dom-tbody-scrollableContainer.html)
20
+ * [Grid](https://catamphetamine.gitlab.io/virtual-scroller/index-dom-grid.html)
21
+ * [Paginated Grid](https://catamphetamine.gitlab.io/virtual-scroller/index-dom-grid.html?pagination=✓)
19
22
 
20
23
  [React](#react) component
21
24
 
22
- * [List](https://catamphetamine.gitlab.io/virtual-scroller/)
23
- * [Paginated List](https://catamphetamine.gitlab.io/virtual-scroller/?dynamic=✓)
24
- * [List in a scrollable container](https://catamphetamine.gitlab.io/virtual-scroller/index-scrollableContainer.html)
25
- * [Grid](https://catamphetamine.gitlab.io/virtual-scroller/index-grid.html)
26
- * [Paginated Grid](https://catamphetamine.gitlab.io/virtual-scroller/index-grid.html?dynamic=✓)
25
+ * [List](https://catamphetamine.gitlab.io/virtual-scroller/index-react.html)
26
+ * [Paginated List](https://catamphetamine.gitlab.io/virtual-scroller/index-react.html?pagination=✓)
27
+ * [List in a scrollable container](https://catamphetamine.gitlab.io/virtual-scroller/index-react-scrollableContainer.html)
28
+ * [Table](https://catamphetamine.gitlab.io/virtual-scroller/index-react-tbody.html)
29
+ * [Table in a scrollable container](https://catamphetamine.gitlab.io/virtual-scroller/index-react-tbody-scrollableContainer.html)
30
+ * [Grid](https://catamphetamine.gitlab.io/virtual-scroller/index-react-grid.html)
31
+ * [Paginated Grid](https://catamphetamine.gitlab.io/virtual-scroller/index-react-grid.html?pagination=✓)
27
32
 
28
33
  ## Rationale
29
34
 
@@ -59,9 +64,9 @@ Alternatively, one could include it on a web page [directly](#cdn) via a `<scrip
59
64
 
60
65
  As it has been mentioned, this package exports three different components:
61
66
 
62
- * For React framework — `virtual-scroller/react`
63
- * For "vanilla" DOM — `virtual-scroller/dom`
64
- * For any other case ("core") — `virtual-scroller`
67
+ * For React framework — [`virtual-scroller/react`](#react)
68
+ * For "vanilla" DOM — [`virtual-scroller/dom`](#dom)
69
+ * For any other case ("core") — [`virtual-scroller`](#core)
65
70
 
66
71
  Below is a description of each component.
67
72
 
@@ -71,9 +76,9 @@ Below is a description of each component.
71
76
 
72
77
  The React component is based on the ["core"](#core) component, and it requires the following properties:
73
78
 
74
- * `items` — The list of items.
79
+ * `items` — an array of items.
75
80
 
76
- * `itemComponent` — The React component for a list item.
81
+ * `itemComponent` — a React component that renders an item.
77
82
 
78
83
  * The `itemComponent` will receive properties:
79
84
  * `item` — The item object (an element of the `items` array). Use it to render the item.
@@ -82,12 +87,20 @@ The React component is based on the ["core"](#core) component, and it requires t
82
87
  <!-- * `state` — The item component's "state". -->
83
88
  <!-- * Curious readers may see the description of `itemStates` property of the `state` object in the ["core"](#core) component section. -->
84
89
  <!-- * `setState(newState)` — Sets the item component's "state". -->
85
- <!-- * Curious readers may see the description of `setItemState(i, newState)` function in the ["core"](#core) component section. -->
90
+ <!-- * Curious readers may see the description of `setItemState(item, newState)` function in the ["core"](#core) component section. -->
86
91
  * `onHeightDidChange()` — Call this function whenever the item's height changes, if it ever does. For example, if the item could be "expanded" and the user clicks that button. The reason for manually calling this function is because `<VirtualScroller/>` only bothers measuring the item's height when the items is initially rendered. After that, it just assumes that the item's height always stays the same and doesn't track it in any way. Hence, a developer is responsible for manually telling it to re-measure the item's height if it has changed for whatever reason.
87
92
  * When calling this function, do it immediately after the item's height has changed on the screen, i.e. do it in `useLayoutEffect()` hook.
88
93
 
89
94
  * As an optional performance optimization, it is advised to wrap the `itemComponent` with a [`React.memo()`](https://react.dev/reference/react/memo) function. It will prevent needless re-renders of the component when its props haven't changed (and they never do). The rationale is that all visible items get frequently re-rendered during scroll.
90
95
 
96
+ * `itemComponentProps: object` — (optional) any additional props for the `itemComponent`.
97
+
98
+ * `itemsContainerComponent` — a React component that will be used as a container for the items.
99
+ * Must be either a simple string like `"div"` or a React component that "forwards" `ref` to the resulting `Element`.
100
+ * Edge case: when list items are rendered as `<tr/>`s and the items container is a `<tbody/>`, the `itemsContainerComponent` must be `"tbody"`, otherwise it won't work correctly.
101
+
102
+ * `itemsContainerComponentProps: object` — (optional) any additional props for the `itemsContainerComponent`.
103
+
91
104
  Code example:
92
105
 
93
106
  #####
@@ -101,6 +114,7 @@ function List({ items }) {
101
114
  <VirtualScroller
102
115
  items={items}
103
116
  itemComponent={ListItem}
117
+ itemsContainerComponent="div"
104
118
  />
105
119
  )
106
120
  }
@@ -152,110 +166,28 @@ function App() {
152
166
  -->
153
167
 
154
168
  <details>
155
- <summary>More on <code>state</code>, <code>setState</code> and <code>onHeightChange()</code></summary>
156
-
157
- #####
158
-
159
- If the `itemComponent` has any internal state, it should be stored in the "virtual scroller" `state` rather than in the usual React state. This is because an item component gets unmounted as soon as it goes off screen, and when it does, all its React state is lost. If the user then scrolls back, the item will be re-rendered "from scratch", without any previous state, which could cause a "jump of content" if the item was somehow "expanded" before it got unmounted.
160
-
161
- For example, consider a social network feed where the feed items (posts) can be expanded or collapsed via a "Show more"/"Show less" button. Suppose a user clicks a "Show more" button in a post resulting in that post expanding in height. Then the user scrolls down, and since the post is no longer visible, it gets unmounted. Since no state is preserved by default, when the user scrolls back up and the post gets mounted again, its previous state will be lost and it will render in a default non-expanded state, resulting in a perceived "jump" of page content by the difference in height between the expanded and non-expanded post state.
162
-
163
- To fix that, `itemComponent` receives the following state management properties:
164
-
165
- * `state` — The state of the item component. It is persisted throughout the entire lifecycle of the list.
166
-
167
- * In the example described above, `state` might look like `{ expanded: true }`.
168
-
169
- * This is simply a proxy for the ["core"](#core) component's `.getState().itemStates[i]`.
170
-
171
- * `setState(newState)` — Use this function to save the item component state whenever it changes.
172
-
173
- * In the example described above, `setState({ expanded: true/false })` would be called whenever a user clicks a "Show more"/"Show less" button.
174
-
175
- * This is simply a proxy for the ["core"](#core) component's `.setItemState(i, newState)`.
176
-
177
- * `onHeightDidChange()` — Call this function immediately after (if ever) the item element height has changed.
178
-
179
- * In the example described above, `onHeightDidChange()` would be called immediately after a user has clicked a "Show more"/"Show less" button and the component has re-rendered itself. Because that sequence of events has resulted in a change of the item element's height, `VirtualScroller` should re-measure the item's height in order for its internal calculations to stay in sync.
180
-
181
- * This is simply a proxy for the ["core"](#core) component's `.onItemHeightDidChange(i)`.
182
-
183
- Example of using `state`/`setState()`/`onHeightDidChange()`:
184
-
185
- ```js
186
- function ItemComponent({
187
- item,
188
- state,
189
- setState,
190
- onHeightDidChange
191
- }) {
192
- const [internalState, setInternalState] = useState(state)
193
-
194
- const hasMounted = useRef()
195
-
196
- useLayoutEffect(() => {
197
- if (hasMounted.current) {
198
- setState(internalState)
199
- onHeightDidChange()
200
- } else {
201
- // Skip the initial mount.
202
- // Only handle the changes of the `internalState`.
203
- hasMounted.current = true
204
- }
205
- }, [internalState])
206
-
207
- return (
208
- <section>
209
- <h1>
210
- {item.title}
211
- </h1>
212
- {internalState && internalState.expanded &&
213
- <p>{item.text}</p>
214
- }
215
- <button onClick={() => {
216
- setInternalState({
217
- ...internalState,
218
- expanded: !expanded
219
- })
220
- }}>
221
- {internalState && internalState.expanded ? 'Show less' : 'Show more'}
222
- </button>
223
- </section>
224
- )
225
- }
226
- ```
227
- </details>
228
-
229
- #####
230
-
231
- <details>
232
- <summary><code>&lt;VirtualScroller/&gt;</code> optional properties</summary>
169
+ <summary>Available options (properties)</summary>
233
170
 
234
171
  #####
235
172
 
236
173
  <!-- Note: When passing any core `VirtualScroller` class options, only the initial values of those options will be applied, and any updates to those options will be ignored. That's because those options are only passed to the `VirtualScroller` base class constructor at initialization time. That means that none of those options should depend on any variable state or props. For example, if `getColumnsCount()` parameter was defined as `() => props.columnsCount`, then, if the `columnsCount` property changes, the underlying `VirtualScroller` instance won't see that change. -->
237
174
 
238
- * `as` — a React component that will be used as a container for the list items. Is `"div"` by default.
239
- * Edge case: when list items are rendered as `<tr/>`s and the items container is a `<tbody/>`, `as` property value must be `"tbody"`, otherwise it won't work correctly.
240
-
241
- * `itemComponentProps: object` — These props will be passed through to the `itemComponent`.
242
-
243
175
  * `getColumnsCount(): number` — Returns the count of the columns.
244
- * This is simply a proxy for the ["core"](#core) component's `getColumnsCount` option.
176
+ * This is simply a proxy for the ["core"](#core) component's `getColumnsCount` [option](#options).
245
177
  * Only the initial value of this property is used, and any changes to it will be ignored.
246
178
 
247
- * `getInitialItemState: (item: any) => any?` — If you're using `state`/`setState()` properties, this function could be used to define the initial `state` for every item in the list. By default, the initial state of an item is `undefined`.
248
- * This is simply a proxy for the ["core"](#core) component's `getInitialItemState` option.
179
+ * `getInitialItemState(item): any?` — If you're using `state`/`setState()` properties, this function could be used to define the initial `state` for every item in the list. By default, the initial state of an item is `undefined`.
180
+ * This is simply a proxy for the ["core"](#core) component's `getInitialItemState` [option](#options).
249
181
  * Only the initial value of this property is used, and any changes to it will be ignored.
250
182
 
251
183
  * `initialState: object` — The initial state of the entire list, including the initial state of each item. For example, one could snapshot this state right before the list is unmounted and then pass it back in the form of the `initialState` property when the list is re-mounted, effectively preserving the list's state. This could be used, for example, to instantly restore the list and its scroll position when the user navigates "Back" to the list's page in a web browser. P.S. In that specific case of using `initialState` property for "Back" restoration, a developer might need to pass `readyToStart: false` property until the "Back" page's scroll position has been restored.
252
- * This is simply a proxy for the ["core"](#core) component's `state` option.
184
+ * This is simply a proxy for the ["core"](#core) component's `state` [option](#options).
253
185
  * Only the initial value of this property is used, and any changes to it will be ignored.
254
186
 
255
187
  <!-- * `initialCustomState: object` — (advanced) The initial "custom" state for `VirtualScroller`: the `initialCustomState` option of `VirtualScroller`. It can be used to initialize the "custom" part of `VirtualScroller` state in cases when `VirtualScroller` state is used to store some "custom" list state. -->
256
188
 
257
189
  * `onStateChange(newState: object, previousState: object?)` — When this function is passed, it will be called every time the list's state is changed. Use it together with `initialState` property to preserve the list's state while it is unmounted.
258
- * This is simply a proxy for the ["core"](#core) component's `onStateChange` option.
190
+ * This is simply a proxy for the ["core"](#core) component's `onStateChange` [option](#options).
259
191
  * Only the initial value of this property is used, and any changes to it will be ignored.
260
192
 
261
193
  Example of using `initialState`/`onStateChange()`:
@@ -287,33 +219,31 @@ function ListWithPreservedState() {
287
219
  }
288
220
  ```
289
221
 
290
- * `getItemId(item): any`
291
- * This is simply a proxy for the ["core"](#core) component's `getItemId` option.
222
+ * `getItemId(item): number | string`
223
+ * This is simply a proxy for the ["core"](#core) component's `getItemId` [option](#options).
292
224
  * Only the initial value of this property is used, and any changes to it will be ignored.
293
225
  * `<VirtualScroller/>` also uses it to create a React `key` for every item's element. When `getItemId()` property is not passed, an item element's `key` will consist of the item's index in the `items` array plus a random-generated prefix that changes every time when `items` property value changes. This means that when the application frequently changes the `items` property, a developer could optimize it a little bit by supplying a custom `getItemId()` function whose result doesn't change when new `items` are supplied, preventing `<VirtualScroller/>` from needlessly re-rendering all visible items every time the `items` property is updated.
294
226
 
295
227
  * `preserveScrollPositionOnPrependItems: boolean` — By default, when prepending new items to the list, the existing items will be pushed downwards on screen. For a user, it would look as if the scroll position has suddenly "jumped", even though technically the scroll position has stayed the same — it's just that the content itself has "jumped". But the user's perception is still that the scroll position has "jumped", as if the application was "buggy". In order to fix such inconvenience, one could pass `true` value here to automatically adjust the scroll position every time when prepending new items to the list. To the end user it would look as if the scroll position is correctly "preserved" when prepending new items to the list, i.e. the application works correctly.
296
- * This is simply a proxy for the ["core"](#core) component's `.setItems()` method's `preserveScrollPositionOnPrependItems` option.
228
+ * This is simply a proxy for the ["core"](#core) component's `.setItems()` method's `preserveScrollPositionOnPrependItems` [option](#options).
297
229
  * Only the initial value of this property is used, and any changes to it will be ignored.
298
230
 
299
231
  * `readyToStart: boolean` — One could initially pass `false` here in order to just initially render the `<VirtualScroller/>` with the provided `initialState` and then hold off calling the `.start()` method of the ["core"](#core) component, effectively "freezing" the `<VirtualScroller/>` until the `false` value is changed to `true`. While in "frozen" state, the `<VirtualScroller/>` will not attempt to re-render itself according to the current scroll position, postponing any such re-renders until `readyToStart` property `false` value is changed to `true`.
300
232
  * An example when this could be required is when a user navigates "Back" to the list's page in a web browser. In that case, the application may use the `initialState` property in an attempt to instantly restore the state of the entire list from a previously-saved snapshot, so that it immediately shows the same items that it was showing before the user navigated away from the list's page. But even if the application passes the previously-snapshotted `initialState`, by default the list will still re-render itself according to the current scroll position. And there wouldn't be any issue with that if the page's scroll position has already been restored to what it was before the user navigated away from the list's page. But if by the time the list is mounted, the page's scroll position hasn't been restored yet, the list will re-render itself with an "incorrect" scroll position, and it will "jump" to completely different items, very unexpectedly to the user, as if the application was "buggy". How could scroll position restoration possibly lag behind? In React it's actually very simple: `<VirtualScroller/>` re-renders itself in a `useLayoutEffect()` hook, which, by React's design, runs before any `useLayoutEffect()` hook in any of the parent components, including the top-level "router" component that handles scroll position restoration on page mount. So it becomes a ["chicken-and-egg"](https://en.wikipedia.org/wiki/Chicken_or_the_egg) problem. And `readyToStart: false` property is the only viable workaround for this dilemma: as soon as the top-level "router" component has finished restoring the scroll position, it could somehow signal that to the rest of the application, and then the application would pass `readyToStart: true` property to the `<VirtualScroller/>` component, unblocking it from re-rendering itself.
301
233
 
302
234
  * `getScrollableContainer(): Element`
303
- * This is simply a proxy for the ["core"](#core) component's `getScrollableContainer` option.
235
+ * This is simply a proxy for the ["core"](#core) component's `getScrollableContainer` [option](#options).
304
236
  * Only the initial value of this property is used, and any changes to it will be ignored.
305
- * This function will first be called only after `<VirtualScroller/>` component is mounted, and by that time all of its ancestors are also mounted. Until then, this function will not be called, so it's completely fine if it returns `undefined` before the component is mounted.
237
+ * This function will be initially called right after `<VirtualScroller/>` component is mounted. However, even though all ancestor DOM Elements already exist in the DOM tree by that time, the corresponding ancestor React Elements haven't "mounted" yet, so their `ref`s are still `null`. This means that `getScrollableContainer()` shouldn't use any `ref`s and should instead get the DOM Element of the scrollable container directly from the `document`.
306
238
 
307
- Example of `getScrollableContainer()`:
239
+ Example of an incorrect `getScrollableContainer()` that won't work:
308
240
 
309
241
  ```js
310
242
  function ListContainer() {
311
243
  const scrollableContainer = useRef()
312
244
 
313
- // This function returns `undefined` until the `<ListContainer/>` is mounted, but it's fine,
314
- // because the `<VirtualScroller/>` will only call it when it itself has mounted, and by that time
315
- // `scrollableContainer.current` will already have been set to a non-`undefined` `HTMLDivElement`.
316
245
  const getScrollableContainer = useCallback(() => {
246
+ // This won't work: it will return `null` because `<ListContainer/>` hasn't "mounted" yet.
317
247
  return scrollableContainer.current
318
248
  }, [])
319
249
 
@@ -328,6 +258,25 @@ function ListContainer() {
328
258
  }
329
259
  ```
330
260
 
261
+ Example of a correct `getScrollableContainer()` that would work:
262
+
263
+ ```js
264
+ function ListContainer() {
265
+ const getScrollableContainer = useCallback(() => {
266
+ return document.getElementById("scrollable-container")
267
+ }, [])
268
+
269
+ return (
270
+ <div id="scrollable-container" style={{ height: "400px", overflow: "scroll" }}>
271
+ <VirtualScroller
272
+ {...}
273
+ getScrollableContainer={getScrollableContainer}
274
+ />
275
+ </div>
276
+ )
277
+ }
278
+ ```
279
+
331
280
  <!--
332
281
  (this property has been ignored for a long time and was eventually removed)
333
282
 
@@ -336,26 +285,34 @@ function ListContainer() {
336
285
  * Only the initial value of this property is used, and any changes to it will be ignored.
337
286
  -->
338
287
 
288
+ * `itemsContainerComponentRef: object` — Could be used to get access to the `itemsContainerComponent` instance.
289
+ * For example, if `itemsContainerComponent` is `"ul"` then `itemsContainerComponentRef.current` will be set to the `<ul/>` `Element`.
290
+
339
291
  * `onItemInitialRender(item)` — When passed, this function will be called for each item when it's rendered for the first time. It could be used to somehow "initialize" an item, if required.
340
- * This is simply a proxy for the ["core"](#core) component's `onItemInitialRender` option.
292
+ * This is simply a proxy for the ["core"](#core) component's `onItemInitialRender` [option](#options).
341
293
  * Only the initial value of this property is used, and any changes to it will be ignored.
342
294
 
343
295
  * `bypass: boolean` — Disables the "virtual" aspect of the list, effectively making it a regular "dumb" list that just renders all items.
344
- * This is simply a proxy for the ["core"](#core) component's `bypass` option.
296
+ * This is simply a proxy for the ["core"](#core) component's `bypass` [option](#options).
345
297
  * Only the initial value of this property is used, and any changes to it will be ignored.
346
298
 
347
- * `getEstimatedItemHeight: () => number` — (unimportant)
348
- * This is simply a proxy for the ["core"](#core) component's `getEstimatedItemHeight` option.
299
+ * `getEstimatedVisibleItemRowsCount(): number` — Should be specified if server-side rendering is used. Can be omitted if server-side rendering is not used.
300
+ * This is simply a proxy for the ["core"](#core) component's `getEstimatedVisibleItemRowsCount` [option](#options).
349
301
  * Only the initial value of this property is used, and any changes to it will be ignored.
350
302
 
351
- * `getEstimatedVisibleItemRowsCount: () => number` — (unimportant)
352
- * This is simply a proxy for the ["core"](#core) component's `getEstimatedVisibleItemRowsCount` option.
303
+ * `getEstimatedItemHeight(): number` — Should be specified if server-side rendering is used. Can be omitted if server-side rendering is not used.
304
+ * This is simply a proxy for the ["core"](#core) component's `getEstimatedItemHeight` [option](#options).
353
305
  * Only the initial value of this property is used, and any changes to it will be ignored.
354
306
 
355
- * `measureItemsBatchSize: number` — (unimportant)
356
- * This is simply a proxy for the ["core"](#core) component's `measureItemsBatchSize` option.
307
+ * `getEstimatedInterItemVerticalSpacing(): number` — Should be specified if server-side rendering is used. Can be omitted if server-side rendering is not used.
308
+ * This is simply a proxy for the ["core"](#core) component's `getEstimatedInterItemVerticalSpacing` [option](#options).
357
309
  * Only the initial value of this property is used, and any changes to it will be ignored.
358
310
 
311
+ * Any other ["core"](#core) component [options](#options) could be passed here.
312
+ * Such as:
313
+ * `measureItemsBatchSize`
314
+ * Only the initial values of those options will be used, any any changes to those will be ignored.
315
+
359
316
  <!-- * `onMount()` — Is called after `<VirtualScroller/>` component has been mounted and before `VirtualScroller.onMount()` is called. -->
360
317
 
361
318
  <!-- * `shouldUpdateLayoutOnScreenResize(event)` — The `shouldUpdateLayoutOnScreenResize` option of `VirtualScroller` class. -->
@@ -412,7 +369,7 @@ function getScrollableContainer() {
412
369
  #####
413
370
 
414
371
  <details>
415
- <summary><code>&lt;VirtualScroller/&gt;</code> instance methods</summary>
372
+ <summary>Instance methods</summary>
416
373
 
417
374
  #####
418
375
 
@@ -422,11 +379,102 @@ function getScrollableContainer() {
422
379
 
423
380
  <!-- * `getItemCoordinates(i)` — A proxy for the corresponding `VirtualScroller` method. -->
424
381
 
382
+ <!-- * `getElement()` — Returns the items container `Element`. -->
383
+
425
384
  * `updateLayout()` — Forces a re-calculation and re-render of the list.
426
385
  * This is simply a proxy for the ["core"](#core) component's `.updateLayout()` method.
427
386
 
428
387
  </details>
429
388
 
389
+ #####
390
+
391
+ <details>
392
+ <summary>More on <code>state</code>, <code>setState</code> and <code>onHeightChange()</code></summary>
393
+
394
+ #####
395
+
396
+ If the `itemComponent` has any internal state, it should be stored in the "virtual scroller" `state` rather than in the usual React state. This is because an item component gets unmounted as soon as it goes off screen, and when it does, all its React state is lost. If the user then scrolls back, the item will be re-rendered "from scratch", without any previous state, which could cause a "jump of content" if the item was somehow "expanded" before it got unmounted.
397
+
398
+ For example, consider a social network feed where the feed items (posts) can be expanded or collapsed via a "Show more"/"Show less" button. Suppose a user clicks a "Show more" button in a post resulting in that post expanding in height. Then the user scrolls down, and since the post is no longer visible, it gets unmounted. Since no state is preserved by default, when the user scrolls back up and the post gets mounted again, its previous state will be lost and it will render in a default non-expanded state, resulting in a perceived "jump" of page content by the difference in height between the expanded and non-expanded post state.
399
+
400
+ To fix that, `itemComponent` receives the following state management properties:
401
+
402
+ * `state` — The state of the item component. It is persisted throughout the entire lifecycle of the list.
403
+
404
+ * In the example described above, `state` might look like `{ expanded: true }`.
405
+
406
+ * This is simply a proxy for the ["core"](#core) component's `.getState().itemStates[i]`.
407
+
408
+ * `setState(newState)` — Use this function to save the item component state whenever it changes.
409
+
410
+ * In the example described above, `setState({ expanded: true/false })` would be called whenever a user clicks a "Show more"/"Show less" button.
411
+
412
+ * This is simply a proxy for the ["core"](#core) component's `.setItemState(item, newState)`.
413
+
414
+ * `onHeightDidChange()` — Call this function immediately after (if ever) the item element height has changed.
415
+
416
+ * In the example described above, `onHeightDidChange()` would be called immediately after a user has clicked a "Show more"/"Show less" button and the component has re-rendered itself. Because that sequence of events has resulted in a change of the item element's height, `VirtualScroller` should re-measure the item's height in order for its internal calculations to stay in sync.
417
+
418
+ * This is simply a proxy for the ["core"](#core) component's `.onItemHeightDidChange(item)`.
419
+
420
+ Example of using `state`/`setState()`/`onHeightDidChange()`:
421
+
422
+ ```js
423
+ function ItemComponent({
424
+ item,
425
+ state,
426
+ setState,
427
+ onHeightDidChange
428
+ }) {
429
+ const [internalState, setInternalState] = useState(state)
430
+
431
+ const hasMounted = useRef()
432
+
433
+ useLayoutEffect(() => {
434
+ if (hasMounted.current) {
435
+ setState(internalState)
436
+ onHeightDidChange()
437
+ } else {
438
+ // Skip the initial mount.
439
+ // Only handle the changes of the `internalState`.
440
+ hasMounted.current = true
441
+ }
442
+ }, [internalState])
443
+
444
+ return (
445
+ <section>
446
+ <h1>
447
+ {item.title}
448
+ </h1>
449
+ {internalState && internalState.expanded &&
450
+ <p>{item.text}</p>
451
+ }
452
+ <button onClick={() => {
453
+ setInternalState({
454
+ ...internalState,
455
+ expanded: !expanded
456
+ })
457
+ }}>
458
+ {internalState && internalState.expanded ? 'Show less' : 'Show more'}
459
+ </button>
460
+ </section>
461
+ )
462
+ }
463
+ ```
464
+ </details>
465
+
466
+ #####
467
+
468
+ <details>
469
+ <summary>Server-Side Render</summary>
470
+
471
+ #####
472
+
473
+ By default, on server side, it will just render the first item, as if the list only had one item. This is because on server side it doesn't know how many items it should render because it doesn't know neither the item height nor the screen height.
474
+
475
+ To fix that, a developer should specify certain properties — `getEstimatedVisibleItemRowsCount(): number` and `getEstimatedItemHeight(): number` and `getEstimatedInterItemVerticalSpacing(): number` — so that it could calculate how many items it should render and how much space it should leave for scrolling. For more technical details, see the description of these parameters in the ["core"](#core) component's [options](#options).
476
+ </details>
477
+
430
478
  ## DOM
431
479
 
432
480
  `virtual-scroller/dom` exports a `VirtualScroller` class that implements a "virtual scroller" in a standard [Document Object Model](https://en.wikipedia.org/wiki/Document_Object_Model) environment such as a web browser.
@@ -511,7 +559,7 @@ const virtualScroller = new VirtualScroller(
511
559
 
512
560
  * `readyToRender: boolean` — The `readyToStart: false` option described above "freezes" the list for any updates but it still performs the initial render of it. If even the initial render of the list should be postponed, pass `readyToRender: false` option, and it will not only prevent the automatic "start" of the `VirtualScroller` at creation time, but it will also prevent the automatic initial render of it until the developer manually calls `.start()` instance method.
513
561
 
514
- * Any other options are simply passed through to the ["core"](#core) component.
562
+ * Any other [options](#options) are simply passed through to the ["core"](#core) component.
515
563
  </details>
516
564
 
517
565
  #####
@@ -526,15 +574,15 @@ The following instance methods are just proxies for the corresponding methods of
526
574
  * `start()`
527
575
  * `stop()`
528
576
  * `setItems(items, options)`
529
- * `setItemState(i, itemState)`
530
- * `onItemHeightDidChange(i)`
577
+ * `setItemState(item, itemState)`
578
+ * `onItemHeightDidChange(item)`
531
579
 
532
- <!-- * `getItemCoordinates(i)` -->
580
+ <!-- * `getItemCoordinates(item)` -->
533
581
  </details>
534
582
 
535
583
  ## Core
536
584
 
537
- The default export is a low-level `VirtualScroller` class: it implements the core logic of a "virtual scroller" component and can be used to build a "virtual scroller" for any UI framework or even any [rendering engine](#rendering-engine) other than DOM. This core class is not meant to be used in applications directly. Instead, prefer using one of the high-level components provided by this library: [`virtual-scroller/react`](#react) or [`virtual-scroller/dom`](#dom). Or implement your own: see `source/test` folder for an example of using the core component to build an "imaginary" renderer implementation.
585
+ The default export is a "core" `VirtualScroller` class: it implements the core logic of a "virtual scroller" component and can be used to build a "virtual scroller" for any UI framework or even any [rendering engine](#rendering-engine) other than DOM. This core class is not meant to be used in applications directly. Instead, prefer using one of the high-level components provided by this library: [`virtual-scroller/react`](#react) or [`virtual-scroller/dom`](#dom). Or implement your own: see `source/test` folder for an example of using the core component to build an "imaginary" renderer implementation.
538
586
 
539
587
  ### State
540
588
 
@@ -553,7 +601,7 @@ Sometimes though, by design, re-rendering could only be done "asynchronously" (i
553
601
 
554
602
  The main `state` properties are:
555
603
 
556
- * `items: any[]` — The list of items (can be updated via [`.setItems()`](#dynamically-loaded-lists)).
604
+ * `items: any[]` — The list of items (can be updated via [`.setItems()`](#updating-items)).
557
605
 
558
606
  * `firstShownItemIndex: number` — The index of the first item that should be rendered.
559
607
 
@@ -567,19 +615,19 @@ The following `state` properties are only used for saving and restoring `Virtual
567
615
 
568
616
  * `itemStates: any[]` — The "states" of all items. If an item's appearance is not "static" and could change, then every aspect of the item's appearance that could change should be represented in the item's "state", and that "state" must be preserved somewhere. That's because of the nature of how `VirtualScroller` works: no-longer-visible items get un-rendered, and when they later become visible again, they should precisely restore their latest-rendered appearance by re-rendering from a previously preserved "state".
569
617
 
570
- * The item "state" could be preserved anywhere in the application, or the developer could use `VirtualScroller`'s built-in item "state" storage. To preserve an item's state in the built-in storage, call `.setItemState(i, itemState)` instance method (described below) immediately after an item's state has changed.
618
+ * The item "state" could be preserved anywhere in the application, or the developer could use `VirtualScroller`'s built-in item "state" storage. To preserve an item's state in the built-in storage, call `.setItemState(item, itemState)` instance method (described below) immediately after an item's state has changed.
571
619
 
572
- * An example would be an item representing a social media comment, with a "Show more"/"Show less" button that shows or hides the full text of the comment. Immediately after the full text of a comment has been shown or hidden, it should call `.setItemState(i, { showMore: true/false })` instance method along with `.onItemHeightDidChange(i)` instance method (described below), so that next time when the item is rendered, it could restore its appearance from `virtualScroller.getState().itemStates[i]`.
620
+ * An example would be an item representing a social media comment, with a "Show more"/"Show less" button that shows or hides the full text of the comment. Immediately after the full text of a comment has been shown or hidden, it should call `.setItemState(item, { showMore: true/false })` instance method along with `.onItemHeightDidChange(item)` instance method (described below), so that next time when the item is rendered, it could restore its appearance from `virtualScroller.getState().itemStates[i]`.
573
621
 
574
622
  * For another similar 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 preserved, then the following "glitch" would be observed: 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 again, but the video is not expanded and instead a small thumbnail is shown because there's no previous "state" to restore from.
575
623
 
576
- * In this example, besides preserving the item state itself, one should also call `.onItemHeightDidChange(i)` instance method (described below) right after the YouTube video has been expanded/collapsed.
624
+ * In this example, besides preserving the item state itself, one should also call `.onItemHeightDidChange(item)` instance method (described below) right after the YouTube video has been expanded/collapsed.
577
625
 
578
626
  * `itemHeights: number[]` — The measured heights of all items. If an item's height hasn't been measured yet then it's `undefined`.
579
627
 
580
- * 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)` immediately after the comment text has been expanded or collapsed.
628
+ * By default, items are only measured once: when they're initially rendered. If an item's height changes afterwards, then `.onItemHeightDidChange(item)` 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(item)` immediately after the comment text has been expanded or collapsed.
581
629
 
582
- * Besides the requirement of calling `.onItemHeightDidChange(i)`, every change in an item's height must also be reflected in the actual data: the change in height must be either a result of the item's internal properties changing or it could be a result of changing the item's "state". The reason is that when an item gets hidden, it's no longer rendered, so when it becomes visible again, it should precisely restore its last-rendered appearance based on the item's properties and any persisted "state".
630
+ * Besides the requirement of calling `.onItemHeightDidChange(item)`, every change in an item's height must also be reflected in the actual data: the change in height must be either a result of the item's internal properties changing or it could be a result of changing the item's "state". The reason is that when an item gets hidden, it's no longer rendered, so when it becomes visible again, it should precisely restore its last-rendered appearance based on the item's properties and any persisted "state".
583
631
 
584
632
  * `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.
585
633
 
@@ -630,112 +678,18 @@ virtualScroller.start()
630
678
  virtualScroller.stop()
631
679
  ```
632
680
 
633
- * `getContainerElement()` Returns the list items container "element".
634
- * `items` — The list of items.
635
- * `render(state, prevState)` — "re-renders" the list.
636
-
637
- #####
638
-
639
- <details>
640
- <summary>An example of implementing a high-level <code>virtual-scroller/dom</code> component on top of the core <code>VirtualScroller</code> component.
641
- </summary>
681
+ `VirtualScroller` class constructor arguments:
642
682
 
643
- #####
644
-
645
- ```js
646
- import VirtualScroller from 'virtual-scroller'
647
-
648
- const items = [
649
- { title: 'Apple' },
650
- { title: 'Banana' },
651
- { title: 'Cranberry' }
652
- ]
653
-
654
- function renderItem(item) {
655
- const div = document.createElement('div')
656
- div.innerText = item.title
657
- return div
658
- }
659
-
660
- const container = document.getElementById('list')
661
-
662
- function render(state, prevState) {
663
- const {
664
- items,
665
- beforeItemsHeight,
666
- afterItemsHeight,
667
- firstShownItemIndex,
668
- lastShownItemIndex
669
- } = state
670
-
671
- // Set `paddingTop` and `paddingBottom` on the container element:
672
- // it emulates the non-visible items as if they were rendered.
673
- container.style.paddingTop = Math.round(beforeItemsHeight) + 'px'
674
- container.style.paddingBottom = Math.round(afterItemsHeight) + 'px'
675
-
676
- // Perform an intelligent "diff" re-render as the user scrolls the page.
677
- // This also requires that the list of `items` hasn't been changed.
678
- // On initial render, `prevState` is `undefined`.
679
- if (prevState && items === prevState.items) {
680
-
681
- // Remove no longer visible items.
682
- let i = prevState.lastShownItemIndex
683
- while (i >= prevState.firstShownItemIndex) {
684
- if (i >= firstShownItemIndex && i <= lastShownItemIndex) {
685
- // The item is still visible.
686
- } else {
687
- // The item is no longer visible. Remove it.
688
- container.removeChild(container.childNodes[i - prevState.firstShownItemIndex])
689
- }
690
- i--
691
- }
692
-
693
- // Add newly visible items.
694
- let prependBefore = container.firstChild
695
- let i = firstShownItemIndex
696
- while (i <= lastShownItemIndex) {
697
- if (i >= prevState.firstShownItemIndex && i <= prevState.lastShownItemIndex) {
698
- // The item is already being rendered.
699
- // Next items will be appended rather than prepended.
700
- prependBefore = undefined
701
- } else {
702
- if (prependBefore) {
703
- container.insertBefore(renderItem(items[i]), prependBefore)
704
- } else {
705
- container.appendChild(renderItem(items[i]))
706
- }
707
- }
708
- i++
709
- }
710
- }
711
- else {
712
- // Re-render the list from scratch.
713
- while (container.firstChild) {
714
- container.removeChild(container.firstChild)
715
- }
716
- let i = firstShownItemIndex
717
- while (i <= lastShownItemIndex) {
718
- container.appendChild(renderItem(items[i]))
719
- i++
720
- }
721
- }
722
- }
723
-
724
- const virtualScroller = new VirtualScroller(() => element, items, { render })
725
-
726
- // Start VirtualScroller listening for scroll events.
727
- virtualScroller.start()
728
-
729
- // Stop VirtualScroller listening for scroll events
730
- // when the user navigates to another page:
731
- // router.onPageUnload(virtualScroller.stop)
732
- ```
733
- </details>
683
+ * `getContainerElement()` — returns the container "element" for the list item "elements".
684
+ * `items` — an array of items.
685
+ * `options` — (optional)
686
+ * `render(state, prevState)` — "re-renders" the list according to the new `state`.
687
+ * The `render()` function can only be specified when it immediately re-renders the list. Sometimes, an immediate re-render is not possible. For example, in React framework, re-render is done "asynchronously", i.e. with a short delay. In such case, instead of specifying a `render` parameter when creating a `virtualScroller` instance, one should omit it and then call an instance method — `virtualScroller.useState({ getState, setState/updateState })` — where `getState` function returns the currently-rendered state and `setState/updateState` function is responsible for triggerring an eventual "re-render" of the list according to the new `state`.
734
688
 
735
689
  #### Options
736
690
 
737
691
  <details>
738
- <summary><code>VirtualScroller</code> <code>options</code></summary>
692
+ <summary>Available <code>options</code></summary>
739
693
 
740
694
  #####
741
695
 
@@ -759,11 +713,20 @@ virtualScroller.start()
759
713
 
760
714
  * `getColumnsCount(container: ScrollableContainer): number` — (advanced) Provides support for ["grid"](#grid-layout) layout. Should return the columns count. The `container` argument provides a `.getWidth()` method for getting the available area width.
761
715
 
762
- #### "Advanced" (rarely used) options
716
+ * `getEstimatedVisibleItemRowsCount(): number` and/or `getEstimatedItemHeight(): number` and/or `getEstimatedInterItemVerticalSpacing(): number` — These functions are only used during the initial render of the list, i.e. when `VirtualScroller` doesn't know anything about the item dimensions.
717
+ * `getEstimatedVisibleItemRowsCount()` is used to guess how many rows of items should be rendered in order to cover the screen area. Sidenote: It will actually render more items than that, with a "prerender margin" on top and bottom, just to account for future scrolling.
718
+ * `getEstimatedItemHeight()` is used to guess the average item height before any of the items have been rendered yet. This average item height is then used to calculate the size of the scrollbar, i.e. how much the user can scroll. It can also be used to calculate the count of visible rows of items if the screen size is known and `getEstimatedVisibleItemRowsCount()` function is not specified.
719
+ * `getEstimatedInterItemVerticalSpacing()` is used to guess the vertical spacing between the items. It is used to calculate the size of the scrollbar, i.e. how much the user can scroll.
720
+ * After the initial render has finished, the list will measure the heights of the rendered items and will use those values to calculate the average item height, the vertical spacing between the items and the count of visible rows of items, and with these new values it will re-render itself.
721
+ * This means that on client side, `getEstimatedVisibleItemRowsCount()` and `getEstimatedItemHeight()` and `getEstimatedInterItemVerticalSpacing()` don't really matter because the list will immediately re-render itself with the correct measured values anyway, and the user will not even observe the results of the initial render because a follow-up render happens immediately.
722
+ * On server side though, `getEstimatedVisibleItemRowsCount()` and `getEstimatedItemHeight()` and `getEstimatedInterItemVerticalSpacing()` completely determine the output of a "server-side render".
723
+ * When these parameters aren't specified, the list will render just the first item during the initial render.
724
+
725
+ #### "Advanced" (rarely-used) options
763
726
 
764
727
  * `bypass: boolean` — Pass `true` to disable the "virtualization" behavior and just render the entire list of items.
765
728
 
766
- * `getInitialItemState: (item: any) => any?` — Creates the initial state for an item. It can be used to populate the default initial states for list items. By default, an item's state is `undefined`.
729
+ * `getInitialItemState(item): any?` — Creates the initial state for an item. It can be used to populate the default initial states for list items. By default, an item's state is `undefined`.
767
730
 
768
731
  * `initialScrollPosition: number` — If passed, the page will be scrolled to this `scrollY` position.
769
732
 
@@ -771,7 +734,7 @@ virtualScroller.start()
771
734
 
772
735
  <!-- * `customState: object` — (advanced) A developer might want to store some "custom" (additional) state along with the `VirtualScroller` state, for whatever reason. To do that, pass the initial value of such "custom" state as the `customState` option when creating a `VirtualScroller` instance. -->
773
736
 
774
- * `getItemId(item)` — (advanced) When `items` are dynamically updated via `.setItems()`, `VirtualScroller` detects an "incremental" update by comparing "new" and "old" item ["references"](https://codeburst.io/explaining-value-vs-reference-in-javascript-647a975e12a0): this way, `VirtualScroller` can understand that the "new" `items` are (mostly) the same as the "old" `items` when some items get prepended or appended to the list, in which case it doesn't re-render the whole list from scratch, but rather just renders the "new" items that got prepended or appended. Sometimes though, some of the "old" items might get updated: for example, if `items` is a list of comments, then some of those comments might get edited in-between the refreshes. In that case, the edited comment object reference should change in order to indicate that the comment's content has changed and that the comment should be re-rendered (at least that's how it has to be done in React world). At the same time, changing the edited comment object reference would break `VirtualScroller`'s "incremental" update detection, and it would re-render the whole list of comments from scratch, which is not what it should be doing in such cases. So, in cases like this, `VirtualScroller` should have some way to understand that the updated item, even if its object reference has changed, is still the same as the old one, so that it doesn't break "incremental" update detection. For that, `getItemId(item)` parameter could be passed, which `VirtualScroller` would use to compare "old" and "new" items (instead of the default "reference equality" check), and that would fix the "re-rendering the whole list from scratch" issue. It can also be used when `items` are fetched from an external API, in which case all item object references change on every such fetch.
737
+ * `getItemId(item): number | string` — (advanced) When `items` are dynamically updated via `.setItems()`, `VirtualScroller` detects an "incremental" update by comparing "new" and "old" item ["references"](https://codeburst.io/explaining-value-vs-reference-in-javascript-647a975e12a0): this way, `VirtualScroller` can understand that the "new" `items` are (mostly) the same as the "old" `items` when some items get prepended or appended to the list, in which case it doesn't re-render the whole list from scratch, but rather just renders the "new" items that got prepended or appended. Sometimes though, some of the "old" items might get updated: for example, if `items` is a list of comments, then some of those comments might get edited in-between the refreshes. In that case, the edited comment object reference should change in order to indicate that the comment's content has changed and that the comment should be re-rendered (at least that's how it has to be done in React world). At the same time, changing the edited comment object reference would break `VirtualScroller`'s "incremental" update detection, and it would re-render the whole list of comments from scratch, which is not what it should be doing in such cases. So, in cases like this, `VirtualScroller` should have some way to understand that the updated item, even if its object reference has changed, is still the same as the old one, so that it doesn't break "incremental" update detection. For that, `getItemId(item)` parameter could be passed, which `VirtualScroller` would use to compare "old" and "new" items (instead of the default "reference equality" check), and that would fix the "re-rendering the whole list from scratch" issue. It can also be used when `items` are fetched from an external API, in which case all item object references change on every such fetch.
775
738
 
776
739
  * `onItemInitialRender(item)` — (advanced) Will be called for each `item` when it's about to be rendered for the first time. This function could be used to somehow "initialize" an item before it gets rendered for the first time. For example, consider a list of items that must be somehow "preprocessed" (parsed, enhanced, etc) before being rendered, and such "preprocessing" puts some load on the CPU (and therefore takes some time). In such case, instead of "preprocessing" the whole list of items up front, the application could "preprocess" only the items that're actually visible, preventing the unnecessary work and reducing the "time to first render".
777
740
  * The function is guaranteed to be called at least once for each item that ever gets rendered.
@@ -784,17 +747,15 @@ virtualScroller.start()
784
747
 
785
748
  * `measureItemsBatchSize: number` — (advanced) (experimental) Imagine a situation when a user doesn't gradually scroll through a huge list but instead hits an End key to scroll right to the end of such huge list: this will result in the whole list rendering at once, because an item needs to know the height of all previous items in order to render at correct scroll position, which could be CPU-intensive in some cases — for example, when using React due to its slow performance when initially rendering components on a page. To prevent freezing the UI in the process, a `measureItemsBatchSize` could be configured, that would limit the maximum count of items that're being rendered in a single pass for measuring their height: if `measureItemsBatchSize` is configured, then such items will be rendered and measured in batches. By default it's set to `100`. This is an experimental feature and could be removed in future non-major versions of this library. For example, the future React 17 will come with [Fiber](https://www.youtube.com/watch?v=ZCuYPiUIONs) rendering engine that is said to resolve such freezing issues internally. In that case, introducing this option may be reconsidered.
786
749
 
787
- <!-- * (alternative description) `getEstimatedItemHeight: () => number` — By default, `<VirtualScroller/>` uses an average measured item height as an estimate for the height of any item that hasn't been rendered yet. This way, it is able to guess what will be the total height of the items below the current scroll position, which is required in order to display a correct scrollbar. However, if the application thinks that it has a better idea of what the average item height is gonna be, it could force `<VirtualScroller/>` to use that value instead of the average measured one. -->
788
-
789
- * `getEstimatedItemHeight: () => number` or `getEstimatedVisibleItemRowsCount: () => number` — These functions are only used during the initial render of the list to estimate how many of the list items should be rendered initially to cover the screen space plus some extra vertical margin (called "prerender margin") for future scrolling. The list will then immediately re-render itself the second time anyway, just to make sure that the layout has been calculated correctly, so the values returned by these functions doesn't have to be 100% accurate. But the more accurate they are, the more accurate is the initial render. The only purpose for the existence of these parameters is eliminating any potential initial "flash" of empty space on screen caused by the list of items being only half-rendered. These two parameters are mutually exclusive: one may only provide one of them. If none of these parameters is provided, then the list initially renders with just the first item being shown, then measures the first item's size, and then rerenders itself again using the measured item height as a substitute for `getEstimatedItemHeight()`.
750
+ <!-- * (alternative description) `getEstimatedItemHeight(): number` — By default, `<VirtualScroller/>` uses an average measured item height as an estimate for the height of any item that hasn't been rendered yet. This way, it is able to guess what will be the total height of the items below the current scroll position, which is required in order to display a correct scrollbar. However, if the application thinks that it has a better idea of what the average item height is gonna be, it could force `<VirtualScroller/>` to use that value instead of the average measured one. -->
790
751
 
791
- * `prerenderMargin` — The list component renders not only the items that're currently visible but also the items that lie within some extra vertical margin (called "prerender margin") on top and bottom for future scrolling: this way, there'll be significantly less layout recalculations as the user scrolls, because now it doesn't have to recalculate layout on each scroll event. By default, the "prerender margin" is equal to the screen height: this seems to be the optimal value for "Page Up" / "Page Down" navigation and optimized mouse wheel scrolling. This parameter is currently ignored because the default value seems to fit all possible use cases.
752
+ * `prerenderMarginRatio` — (currently unused) The list component renders not only the items that're currently visible but also the items that lie within some additional vertical distance (called "prerender margin") on top and bottom to account for future scrolling. This way, it doesn't have to recalculate the layout on each scroll event and is only forced to recalculate the layout if the user scrolls past the "prerender margin". Therefore, "prerender margin" is an optimization that "throttles" layout recalculation. By default, the "prerender margin" is equal to scrollable container height: this seems to be the most optimal value to account for "Page Up" / "Page Down" scrolling. This parameter is currently not customizable because the default value of `1` seems to work fine in all possible use cases.
792
753
  </details>
793
754
 
794
755
  #####
795
756
 
796
757
  <details>
797
- <summary><code>VirtualScroller</code> instance methods</summary>
758
+ <summary>Instance methods</summary>
798
759
 
799
760
  #####
800
761
 
@@ -804,7 +765,7 @@ virtualScroller.start()
804
765
 
805
766
  * `getState(): object` — Returns `VirtualScroller` state.
806
767
 
807
- * `setItems(newItems: any[], options: object?)` — Updates `VirtualScroller` `items`. For example, it can be used to prepend or append new items to the list. See [Dynamically Loaded Lists](#dynamically-loaded-lists) section for more details. Available options:
768
+ * `setItems(newItems: any[], options: object?)` — Updates `VirtualScroller` `items`. For example, it can be used to prepend or append new items to the list. See [Updating Items](#updating-items) section for more details. Available options:
808
769
  * `preserveScrollPositionOnPrependItems: boolean` — Set to `true` to enable "restore scroll position after prepending new items" feature (should be used when implementing a "Show previous items" button).
809
770
 
810
771
  #### Custom (External) State Management
@@ -841,25 +802,123 @@ When using custom (external) state management, contrary to the default (internal
841
802
 
842
803
  #### "Advanced" (rarely used) instance methods
843
804
 
844
- * `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. The function re-measures the item's height (the item must still be rendered) and re-calculates `VirtualScroller` layout. An example for using this function would be having an "Expand"/"Collapse" button in a list item.
805
+ * `onItemHeightDidChange(item)` — (advanced) If an item's height could've changed, this function should be called immediately after the item's height has potentially changed. The function re-measures the item's height (the item must still be rendered) and re-calculates `VirtualScroller` layout. An example for using this function would be having an "Expand"/"Collapse" button in a list item.
845
806
 
846
807
  * There's also a convention that every change in an item's height must come as a result of changing the item's "state". See the descripton of `itemStates` and `itemHeights` properties of the `VirtualScroller` [state](#state) for more details.
847
808
 
848
- * 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.
809
+ * Implementation-wise, calling `onItemHeightDidChange(item)` manually could be replaced with detecting item height changes automatically via [Resize Observer](https://caniuse.com/#search=Resize%20Observer) in some future version.
849
810
 
850
- * `setItemState(i: number, itemState: any?)` — (advanced) Preserves a list item's "state" inside `VirtualScroller`'s built-in item "state" storage. See the descripton of `itemStates` property of the `VirtualScroller` [state](#state) for more details.
811
+ * `setItemState(item, itemState: any?)` — (advanced) Preserves a list item's "state" inside `VirtualScroller`'s built-in item "state" storage. See the descripton of `itemStates` property of the `VirtualScroller` [state](#state) for more details.
851
812
 
852
813
  * A developer could use it to preserve an item's "state" if it could change. The reason is that offscreen items get unmounted and any unsaved state is lost in the process. If an item's state is correctly preserved, the item's latest-rendered appearance could be restored from that state when the item gets mounted again due to becoming visible again.
853
814
 
854
- * 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, one should call `onItemHeightDidChange(i)` right after the change in the item's state has been reflected on screen.
815
+ * 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, one should call `onItemHeightDidChange(item)` right after the change in the item's state has been reflected on screen.
855
816
 
856
- * `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.
817
+ * `getItemScrollPosition(item): 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.
857
818
 
858
- <!-- * `getItemCoordinates(i: number): object` — Returns coordinates of item with index `i` relative to the "scrollable container": `top` is the top offset of the item relative to the start of the "scrollable container", `bottom` is the top offset of the item's bottom edge relative to the start of the "scrollable container", `height` is the item's height. -->
819
+ <!-- * `getItemCoordinates(item): object` — Returns coordinates of item with index `i` relative to the "scrollable container": `top` is the top offset of the item relative to the start of the "scrollable container", `bottom` is the top offset of the item's bottom edge relative to the start of the "scrollable container", `height` is the item's height. -->
859
820
 
860
821
  * `updateLayout()` — (advanced) Triggers a re-layout of `VirtualScroller`. It's what's called every time on page scroll or window resize. You most likely won't ever need to call this method manually. Still, one could imagine a hypothetical case when a developer might want to call this method. For example, when the list's top position changes not as a result of scrolling the page or resizing the window, but rather because of some unrelated "dynamic" changes of the page's content. For example, if some DOM elements above the list are removed (like a closeable "info" notification element) or collapsed (like an "accordion" panel), then the list's top position changes, which means that now some of the previoulsy shown items might go off screen, revealing an unrendered blank area to the user. The area would be blank because the "shift" of the list's vertical position happened not as a result of the user scrolling the page or resizing the window, and, therefore, it won't be registered by the `VirtualScroller` component. To fix that, a developer might want to trigger a re-layout manually.
861
822
  </details>
862
823
 
824
+ #####
825
+
826
+ <details>
827
+ <summary>Example: implement <code>virtual-scroller/dom</code> component using the "core" <code>VirtualScroller</code> component
828
+ </summary>
829
+
830
+ #####
831
+
832
+ ```js
833
+ import VirtualScroller from 'virtual-scroller'
834
+
835
+ const items = [
836
+ { title: 'Apple' },
837
+ { title: 'Banana' },
838
+ { title: 'Cranberry' }
839
+ ]
840
+
841
+ function renderItem(item) {
842
+ const div = document.createElement('div')
843
+ div.innerText = item.title
844
+ return div
845
+ }
846
+
847
+ const container = document.getElementById('list')
848
+
849
+ function render(state, prevState) {
850
+ const {
851
+ items,
852
+ beforeItemsHeight,
853
+ afterItemsHeight,
854
+ firstShownItemIndex,
855
+ lastShownItemIndex
856
+ } = state
857
+
858
+ // Set `paddingTop` and `paddingBottom` on the container element:
859
+ // it emulates the non-visible items as if they were rendered.
860
+ container.style.paddingTop = Math.round(beforeItemsHeight) + 'px'
861
+ container.style.paddingBottom = Math.round(afterItemsHeight) + 'px'
862
+
863
+ // Perform an intelligent "diff" re-render as the user scrolls the page.
864
+ // This also requires that the list of `items` hasn't been changed.
865
+ // On initial render, `prevState` is `undefined`.
866
+ if (prevState && items === prevState.items) {
867
+
868
+ // Remove no longer visible items.
869
+ let i = prevState.lastShownItemIndex
870
+ while (i >= prevState.firstShownItemIndex) {
871
+ if (i >= firstShownItemIndex && i <= lastShownItemIndex) {
872
+ // The item is still visible.
873
+ } else {
874
+ // The item is no longer visible. Remove it.
875
+ container.removeChild(container.childNodes[i - prevState.firstShownItemIndex])
876
+ }
877
+ i--
878
+ }
879
+
880
+ // Add newly visible items.
881
+ let prependBefore = container.firstChild
882
+ let i = firstShownItemIndex
883
+ while (i <= lastShownItemIndex) {
884
+ if (i >= prevState.firstShownItemIndex && i <= prevState.lastShownItemIndex) {
885
+ // The item is already being rendered.
886
+ // Next items will be appended rather than prepended.
887
+ prependBefore = undefined
888
+ } else {
889
+ if (prependBefore) {
890
+ container.insertBefore(renderItem(items[i]), prependBefore)
891
+ } else {
892
+ container.appendChild(renderItem(items[i]))
893
+ }
894
+ }
895
+ i++
896
+ }
897
+ }
898
+ else {
899
+ // Re-render the list from scratch.
900
+ while (container.firstChild) {
901
+ container.removeChild(container.firstChild)
902
+ }
903
+ let i = firstShownItemIndex
904
+ while (i <= lastShownItemIndex) {
905
+ container.appendChild(renderItem(items[i]))
906
+ i++
907
+ }
908
+ }
909
+ }
910
+
911
+ const virtualScroller = new VirtualScroller(() => element, items, { render })
912
+
913
+ // Start VirtualScroller listening for scroll events.
914
+ virtualScroller.start()
915
+
916
+ // Stop VirtualScroller listening for scroll events
917
+ // when the user navigates to another page:
918
+ // router.onPageUnload(virtualScroller.stop)
919
+ ```
920
+ </details>
921
+
863
922
  ## Updating Items
864
923
 
865
924
  If the list represents a social media feed, it has to be updated periodically as new posts get published.
@@ -1129,7 +1188,7 @@ Due to the [inherent limitations](https://gitlab.com/catamphetamine/virtual-scro
1129
1188
 
1130
1189
  ### "Item index N height changed unexpectedly" warning on page load in development mode
1131
1190
 
1132
- `VirtualScroller` assumes there'd be no "unexpected" (unannounced) changes in items' heights. If an item's height changes for whatever reason, a developer must announce it immediately by calling `.onItemHeightDidChange(i)` instance method.
1191
+ `VirtualScroller` assumes there'd be no "unexpected" (unannounced) changes in items' heights. If an item's height changes for whatever reason, a developer must announce it immediately by calling `.onItemHeightDidChange(item)` instance method.
1133
1192
 
1134
1193
  There might still be cases outside of a developer's control when items' heights do change "unexpectedly". One such case is when running an application in "development" mode in a bundler such as Webpack, and the CSS styles or custom fonts haven't loaded yet, resulting in different item height measurements "before" and "after" the page has fully loaded. Note that this is not a bug of `VirtualScroller`. It's just an inconvenience introduced by a bundler such as Webpack, and only in "development" mode, i.e. it won't happen in production.
1135
1194
 
@@ -1270,7 +1329,7 @@ To include this library directly via a `<script/>` tag on a page, one can use an
1270
1329
  <!--
1271
1330
  ## Possible enhancements
1272
1331
 
1273
- * Use [Resize Observer](https://caniuse.com/#search=Resize%20Observer) instead of calling `.onItemHeightDidChange(i)` manually.
1332
+ * Use [Resize Observer](https://caniuse.com/#search=Resize%20Observer) instead of calling `.onItemHeightDidChange(item)` manually.
1274
1333
 
1275
1334
  * Currently React `<VirtualScroller/>` passes `onHeightDidChange()` property and provides `.renderItem(i)` instance method. Both these features could be replaced with doing it internally in `VirtualScroller`'s `.setItems(newItems)` method: it could detect the items that have changed (`prevItems[i] !== newItems[i]`) and recalculate heights for such items, while the changed `item` properties would also cause the relevant React elements to be rerendered.
1276
1335
  -->