virtual-scroller 1.11.3 → 1.12.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.
- package/README.md +6 -4
- package/bundle/virtual-scroller-dom.js +1 -1
- package/bundle/virtual-scroller-dom.js.map +1 -1
- package/bundle/virtual-scroller-react.js +1 -1
- package/bundle/virtual-scroller-react.js.map +1 -1
- package/bundle/virtual-scroller.js +1 -1
- package/bundle/virtual-scroller.js.map +1 -1
- package/commonjs/DOM/ItemsContainer.js +10 -3
- package/commonjs/DOM/ItemsContainer.js.map +1 -1
- package/commonjs/ItemNotRenderedError.js +64 -0
- package/commonjs/ItemNotRenderedError.js.map +1 -0
- package/commonjs/Layout.test.js +10 -0
- package/commonjs/Layout.test.js.map +1 -1
- package/commonjs/VirtualScroller.js +2 -1
- package/commonjs/VirtualScroller.js.map +1 -1
- package/commonjs/VirtualScroller.layout.js +61 -19
- package/commonjs/VirtualScroller.layout.js.map +1 -1
- package/commonjs/VirtualScroller.onRender.js +97 -45
- package/commonjs/VirtualScroller.onRender.js.map +1 -1
- package/commonjs/VirtualScroller.state.js +50 -18
- package/commonjs/VirtualScroller.state.js.map +1 -1
- package/commonjs/react/VirtualScroller.js +26 -42
- package/commonjs/react/VirtualScroller.js.map +1 -1
- package/commonjs/react/useItemKeys.js +11 -3
- package/commonjs/react/useItemKeys.js.map +1 -1
- package/commonjs/react/useOnChange.js +19 -0
- package/commonjs/react/useOnChange.js.map +1 -0
- package/commonjs/react/{useHandleItemsPropertyChange.js → useSetNewItemsOnItemsPropertyChange.js} +15 -14
- package/commonjs/react/useSetNewItemsOnItemsPropertyChange.js.map +1 -0
- package/commonjs/react/useState.js +162 -69
- package/commonjs/react/useState.js.map +1 -1
- package/commonjs/react/useStyle.js +3 -5
- package/commonjs/react/useStyle.js.map +1 -1
- package/commonjs/react/useUpdateItemKeysOnItemsChange.js +61 -0
- package/commonjs/react/useUpdateItemKeysOnItemsChange.js.map +1 -0
- package/commonjs/test/ItemsContainer.js +22 -1
- package/commonjs/test/ItemsContainer.js.map +1 -1
- package/commonjs/utility/debug.js +30 -6
- package/commonjs/utility/debug.js.map +1 -1
- package/index.cjs +2 -0
- package/index.d.ts +6 -0
- package/index.js +1 -0
- package/modules/DOM/ItemsContainer.js +8 -3
- package/modules/DOM/ItemsContainer.js.map +1 -1
- package/modules/ItemNotRenderedError.js +57 -0
- package/modules/ItemNotRenderedError.js.map +1 -0
- package/modules/Layout.test.js +10 -0
- package/modules/Layout.test.js.map +1 -1
- package/modules/VirtualScroller.js +2 -1
- package/modules/VirtualScroller.js.map +1 -1
- package/modules/VirtualScroller.layout.js +58 -19
- package/modules/VirtualScroller.layout.js.map +1 -1
- package/modules/VirtualScroller.onRender.js +98 -46
- package/modules/VirtualScroller.onRender.js.map +1 -1
- package/modules/VirtualScroller.state.js +50 -18
- package/modules/VirtualScroller.state.js.map +1 -1
- package/modules/react/VirtualScroller.js +26 -42
- package/modules/react/VirtualScroller.js.map +1 -1
- package/modules/react/useItemKeys.js +8 -3
- package/modules/react/useItemKeys.js.map +1 -1
- package/modules/react/useOnChange.js +11 -0
- package/modules/react/useOnChange.js.map +1 -0
- package/modules/react/{useHandleItemsPropertyChange.js → useSetNewItemsOnItemsPropertyChange.js} +11 -13
- package/modules/react/useSetNewItemsOnItemsPropertyChange.js.map +1 -0
- package/modules/react/useState.js +156 -73
- package/modules/react/useState.js.map +1 -1
- package/modules/react/useStyle.js +3 -5
- package/modules/react/useStyle.js.map +1 -1
- package/{commonjs/react/useHandleItemIndexesChange.js → modules/react/useUpdateItemKeysOnItemsChange.js} +18 -21
- package/modules/react/useUpdateItemKeysOnItemsChange.js.map +1 -0
- package/modules/test/ItemsContainer.js +20 -1
- package/modules/test/ItemsContainer.js.map +1 -1
- package/modules/utility/debug.js +31 -6
- package/modules/utility/debug.js.map +1 -1
- package/package.json +1 -1
- package/source/DOM/ItemsContainer.js +8 -3
- package/source/ItemNotRenderedError.js +16 -0
- package/source/Layout.test.js +9 -0
- package/source/VirtualScroller.js +2 -0
- package/source/VirtualScroller.layout.js +57 -18
- package/source/VirtualScroller.onRender.js +95 -42
- package/source/VirtualScroller.state.js +57 -20
- package/source/react/VirtualScroller.js +23 -35
- package/source/react/useItemKeys.js +9 -2
- package/source/react/useOnChange.js +11 -0
- package/source/react/{useHandleItemsPropertyChange.js → useSetNewItemsOnItemsPropertyChange.js} +11 -11
- package/source/react/useState.js +159 -71
- package/source/react/useStyle.js +2 -2
- package/source/react/{useHandleItemIndexesChange.js → useUpdateItemKeysOnItemsChange.js} +17 -9
- package/source/test/ItemsContainer.js +22 -1
- package/source/utility/debug.js +18 -4
- package/commonjs/react/useHandleItemIndexesChange.js.map +0 -1
- package/commonjs/react/useHandleItemsPropertyChange.js.map +0 -1
- package/modules/react/useHandleItemIndexesChange.js +0 -45
- package/modules/react/useHandleItemIndexesChange.js.map +0 -1
- package/modules/react/useHandleItemsPropertyChange.js.map +0 -1
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export default class ItemNotRenderedError extends Error {
|
|
2
|
+
constructor({
|
|
3
|
+
renderedElementIndex,
|
|
4
|
+
renderedElementsCount,
|
|
5
|
+
message
|
|
6
|
+
}) {
|
|
7
|
+
super(message || getDefaultMessage({ renderedElementIndex, renderedElementsCount }))
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getDefaultMessage({
|
|
12
|
+
renderedElementIndex,
|
|
13
|
+
renderedElementsCount
|
|
14
|
+
}) {
|
|
15
|
+
return `Element with index ${renderedElementIndex} was not found in the list of Rendered Item Elements in the Items Container of Virtual Scroller. There're only ${renderedElementsCount} Elements there.`
|
|
16
|
+
}
|
package/source/Layout.test.js
CHANGED
|
@@ -148,6 +148,10 @@ describe('Layout', function() {
|
|
|
148
148
|
|
|
149
149
|
let shouldResetGridLayout
|
|
150
150
|
|
|
151
|
+
const errors = []
|
|
152
|
+
|
|
153
|
+
global.VirtualScrollerCatchError = (error) => errors.push(error)
|
|
154
|
+
|
|
151
155
|
layout.getLayoutUpdateForItemsDiff(
|
|
152
156
|
{
|
|
153
157
|
firstShownItemIndex: 3,
|
|
@@ -171,6 +175,11 @@ describe('Layout', function() {
|
|
|
171
175
|
afterItemsHeight: 5 * (ITEM_HEIGHT + VERTICAL_SPACING)
|
|
172
176
|
})
|
|
173
177
|
|
|
178
|
+
global.VirtualScrollerCatchError = undefined
|
|
179
|
+
errors.length.should.equal(2)
|
|
180
|
+
errors[0].message.should.equal('[virtual-scroller] ~ Prepended items count 5 is not divisible by Columns Count 4 ~')
|
|
181
|
+
errors[1].message.should.equal('[virtual-scroller] Layout reset required')
|
|
182
|
+
|
|
174
183
|
shouldResetGridLayout.should.equal(true)
|
|
175
184
|
})
|
|
176
185
|
})
|
|
@@ -35,6 +35,8 @@ export default class VirtualScroller {
|
|
|
35
35
|
const isRestart = this._isActive === false
|
|
36
36
|
|
|
37
37
|
if (!isRestart) {
|
|
38
|
+
this.waitingForRender = true
|
|
39
|
+
|
|
38
40
|
// If no custom state storage has been configured, use the default one.
|
|
39
41
|
// Also sets the initial state.
|
|
40
42
|
if (!this._usesCustomStateStorage) {
|
|
@@ -7,6 +7,8 @@ import { setTimeout, clearTimeout } from 'request-animation-frame-timeout'
|
|
|
7
7
|
import log, { warn, isDebug, reportError } from './utility/debug.js'
|
|
8
8
|
import { LAYOUT_REASON } from './Layout.js'
|
|
9
9
|
|
|
10
|
+
import ItemNotRenderedError from './ItemNotRenderedError.js'
|
|
11
|
+
|
|
10
12
|
export default function() {
|
|
11
13
|
this.onUpdateShownItemIndexes = ({ reason, stateUpdate }) => {
|
|
12
14
|
// In case of "don't do anything".
|
|
@@ -149,6 +151,9 @@ export default function() {
|
|
|
149
151
|
|
|
150
152
|
// Set `this.firstNonMeasuredItemIndex`.
|
|
151
153
|
this.firstNonMeasuredItemIndex = firstNonMeasuredItemIndex
|
|
154
|
+
// if (firstNonMeasuredItemIndex !== undefined) {
|
|
155
|
+
// log('Non-measured item index that will be measured at next layout', firstNonMeasuredItemIndex)
|
|
156
|
+
// }
|
|
152
157
|
|
|
153
158
|
// Set "previously calculated layout".
|
|
154
159
|
//
|
|
@@ -340,20 +345,21 @@ export default function() {
|
|
|
340
345
|
// rather than from scratch, which would be an optimization.
|
|
341
346
|
//
|
|
342
347
|
function updatePreviouslyCalculatedLayoutOnItemHeightChange(i, previousHeight, newHeight) {
|
|
343
|
-
|
|
348
|
+
const prevLayout = this.previouslyCalculatedLayout
|
|
349
|
+
if (prevLayout) {
|
|
344
350
|
const heightDifference = newHeight - previousHeight
|
|
345
|
-
if (i <
|
|
346
|
-
// Patch `
|
|
347
|
-
|
|
348
|
-
} else if (i >
|
|
349
|
-
// Could patch `.afterItemsHeight` of `
|
|
350
|
-
// if `.afterItemsHeight` property existed in `
|
|
351
|
-
if (
|
|
352
|
-
|
|
351
|
+
if (i < prevLayout.firstShownItemIndex) {
|
|
352
|
+
// Patch `prevLayout`'s `.beforeItemsHeight`.
|
|
353
|
+
prevLayout.beforeItemsHeight += heightDifference
|
|
354
|
+
} else if (i > prevLayout.lastShownItemIndex) {
|
|
355
|
+
// Could patch `.afterItemsHeight` of `prevLayout` here,
|
|
356
|
+
// if `.afterItemsHeight` property existed in `prevLayout`.
|
|
357
|
+
if (prevLayout.afterItemsHeight !== undefined) {
|
|
358
|
+
prevLayout.afterItemsHeight += heightDifference
|
|
353
359
|
}
|
|
354
360
|
} else {
|
|
355
|
-
// Patch `
|
|
356
|
-
|
|
361
|
+
// Patch `prevLayout`'s shown items height.
|
|
362
|
+
prevLayout.shownItemsHeight += newHeight - previousHeight
|
|
357
363
|
}
|
|
358
364
|
}
|
|
359
365
|
}
|
|
@@ -371,7 +377,7 @@ export default function() {
|
|
|
371
377
|
}
|
|
372
378
|
|
|
373
379
|
this._onItemHeightDidChange = (i) => {
|
|
374
|
-
log('~
|
|
380
|
+
log('~ On Item Height Did Change was called ~')
|
|
375
381
|
log('Item index', i)
|
|
376
382
|
|
|
377
383
|
const {
|
|
@@ -412,24 +418,57 @@ export default function() {
|
|
|
412
418
|
|
|
413
419
|
const previousHeight = itemHeights[i]
|
|
414
420
|
if (previousHeight === undefined) {
|
|
415
|
-
return reportError(`"onItemHeightDidChange()" has been called for item ${i}
|
|
421
|
+
return reportError(`"onItemHeightDidChange()" has been called for item index ${i} but the item hasn't been rendered before.`)
|
|
416
422
|
}
|
|
417
423
|
|
|
418
|
-
|
|
424
|
+
log('~ Re-measure item height ~')
|
|
425
|
+
|
|
426
|
+
let newHeight
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
newHeight = remeasureItemHeight.call(this, i)
|
|
430
|
+
} catch (error) {
|
|
431
|
+
// Successfully finishing an `onItemHeightDidChange(i)` call is not considered
|
|
432
|
+
// critical for `VirtualScroller`'s operation, so such errors could be ignored.
|
|
433
|
+
if (error instanceof ItemNotRenderedError) {
|
|
434
|
+
return reportError(`"onItemHeightDidChange()" has been called for item index ${i} but the item is not currently rendered and can\'t be measured. The exact error was: ${error.message}`)
|
|
435
|
+
}
|
|
436
|
+
}
|
|
419
437
|
|
|
420
438
|
log('Previous height', previousHeight)
|
|
421
439
|
log('New height', newHeight)
|
|
422
440
|
|
|
423
441
|
if (previousHeight !== newHeight) {
|
|
424
|
-
log('~ Item height has changed ~')
|
|
442
|
+
log('~ Item height has changed. Should update layout. ~')
|
|
425
443
|
|
|
426
|
-
// Update or reset previously calculated layout
|
|
444
|
+
// Update or reset a previously calculated layout
|
|
445
|
+
// so that the "diff"s based on that layout in the future
|
|
446
|
+
// produce correct results.
|
|
427
447
|
updatePreviouslyCalculatedLayoutOnItemHeightChange.call(this, i, previousHeight, newHeight)
|
|
428
448
|
|
|
429
449
|
// Recalculate layout.
|
|
430
|
-
|
|
450
|
+
//
|
|
451
|
+
// If the `VirtualScroller` is already waiting for a state update to be rendered,
|
|
452
|
+
// delay `onItemHeightDidChange(i)`'s re-layout until that state update is rendered.
|
|
453
|
+
// The reason is that React `<VirtualScroller/>`'s `onHeightDidChange()` is meant to
|
|
454
|
+
// be called inside `useLayoutEffect()` hook. Due to how React is implemented internally,
|
|
455
|
+
// that might happen in the middle of the currently pending `setState()` operation
|
|
456
|
+
// being applied, resulting in weird "race condition" bugs.
|
|
457
|
+
//
|
|
458
|
+
if (this.waitingForRender) {
|
|
459
|
+
log('~ Another state update is already waiting to be rendered. Delay the layout update until then. ~')
|
|
460
|
+
this.updateLayoutAfterRenderBecauseItemHeightChanged = true
|
|
461
|
+
} else {
|
|
462
|
+
this.onUpdateShownItemIndexes({ reason: LAYOUT_REASON.ITEM_HEIGHT_CHANGED })
|
|
463
|
+
}
|
|
431
464
|
|
|
432
|
-
//
|
|
465
|
+
// If there was a request for `setState()` with new `items`, then the changes
|
|
466
|
+
// to `currentState.itemHeights[]` made above in a `remeasureItemHeight()` call
|
|
467
|
+
// would be overwritten when that pending `setState()` call gets applied.
|
|
468
|
+
// To fix that, the updates to current `itemHeights[]` are noted in
|
|
469
|
+
// `this.itemHeightsThatChangedWhileNewItemsWereBeingRendered` variable.
|
|
470
|
+
// That variable is then checked when the `setState()` call with the new `items`
|
|
471
|
+
// has been updated.
|
|
433
472
|
if (this.newItemsWillBeRendered) {
|
|
434
473
|
if (!this.itemHeightsThatChangedWhileNewItemsWereBeingRendered) {
|
|
435
474
|
this.itemHeightsThatChangedWhileNewItemsWereBeingRendered = {}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import log, { warn, isDebug } from './utility/debug.js'
|
|
1
|
+
import log, { warn, reportError, isDebug } from './utility/debug.js'
|
|
2
2
|
import getStateSnapshot from './utility/getStateSnapshot.js'
|
|
3
3
|
import shallowEqual from './utility/shallowEqual.js'
|
|
4
4
|
import { LAYOUT_REASON } from './Layout.js'
|
|
@@ -11,6 +11,8 @@ export default function() {
|
|
|
11
11
|
* @param {object} [prevState]
|
|
12
12
|
*/
|
|
13
13
|
this._onRender = (newState, prevState) => {
|
|
14
|
+
this.waitingForRender = false
|
|
15
|
+
|
|
14
16
|
log('~ Rendered ~')
|
|
15
17
|
if (isDebug()) {
|
|
16
18
|
log('State', getStateSnapshot(newState))
|
|
@@ -33,19 +35,58 @@ export default function() {
|
|
|
33
35
|
)
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
// `this.mostRecentlySetState` checks that state management behavior is correct:
|
|
39
|
+
// that in situations when there're multiple new states waiting to be set,
|
|
40
|
+
// only the latest one gets applied.
|
|
41
|
+
// It keeps the code simpler and prevents possible race condition bugs.
|
|
42
|
+
// For example, `VirtualScroller` keeps track of its latest requested
|
|
43
|
+
// state update in different instance variable flags which assume that
|
|
44
|
+
// only that latest requested state update gets actually applied.
|
|
45
|
+
//
|
|
46
|
+
// This check should also be performed for the initial render in order to
|
|
47
|
+
// guarantee that no potentially incorrect state update goes unnoticed.
|
|
48
|
+
// Incorrect state updates could happen when `VirtualScroller` state
|
|
49
|
+
// is managed externally by passing `getState()`/`updateState()` options.
|
|
50
|
+
//
|
|
51
|
+
// Perform the check only when `this.mostRecentSetStateValue` is defined.
|
|
52
|
+
// `this.mostRecentSetStateValue` is normally gonna be `undefined` at the initial render
|
|
53
|
+
// because the initial state is not set by calling `this.updateState()`.
|
|
54
|
+
// At the same time, it is possible that the initial render is delayed
|
|
55
|
+
// for whatever reason, and `this.updateState()` gets called before the initial render,
|
|
56
|
+
// so `this.mostRecentSetStateValue` could also be defined at the initial render,
|
|
57
|
+
// in which case the check should be performed.
|
|
58
|
+
//
|
|
59
|
+
if (this.mostRecentSetStateValue) {
|
|
60
|
+
// "Shallow equality" is used here instead of "strict equality"
|
|
61
|
+
// because a developer might choose to supply an `updateState()` function
|
|
62
|
+
// rather than a `setState()` function, in which case the `updateState()` function
|
|
63
|
+
// would construct its own state object.
|
|
64
|
+
if (!shallowEqual(newState, this.mostRecentSetStateValue)) {
|
|
65
|
+
warn('The most recent state that was set', getStateSnapshot(this.mostRecentSetStateValue))
|
|
66
|
+
reportError('The state that has been rendered is not the most recent one that was set')
|
|
67
|
+
}
|
|
38
68
|
}
|
|
39
69
|
|
|
40
70
|
// `this.resetStateUpdateFlags()` must be called before calling
|
|
41
71
|
// `this.measureItemHeightsAndSpacing()`.
|
|
42
72
|
const {
|
|
43
73
|
nonMeasuredItemsHaveBeenRendered,
|
|
74
|
+
itemHeightHasChanged,
|
|
44
75
|
widthHasChanged
|
|
45
76
|
} = resetStateUpdateFlags.call(this)
|
|
46
77
|
|
|
47
78
|
let layoutUpdateReason
|
|
48
79
|
|
|
80
|
+
if (this.updateLayoutAfterRenderBecauseItemHeightChanged) {
|
|
81
|
+
layoutUpdateReason = LAYOUT_REASON.ITEM_HEIGHT_CHANGED
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!prevState) {
|
|
85
|
+
if (!layoutUpdateReason) {
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
49
90
|
// If the `VirtualScroller`, while calculating layout parameters, encounters
|
|
50
91
|
// a not-shown item with a non-measured height, it calls `updateState()` just to
|
|
51
92
|
// render that item first, and then, after the list has been re-rendered, it measures
|
|
@@ -74,41 +115,43 @@ export default function() {
|
|
|
74
115
|
this.verticalSpacing = undefined
|
|
75
116
|
}
|
|
76
117
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
118
|
+
if (prevState) {
|
|
119
|
+
const { items: previousItems } = prevState
|
|
120
|
+
const { items: newItems } = newState
|
|
121
|
+
// Even if `this.newItemsWillBeRendered` flag is `true`,
|
|
122
|
+
// `newItems` could still be equal to `previousItems`.
|
|
123
|
+
// For example, when `updateState()` calls don't update `state` immediately
|
|
124
|
+
// and a developer first calls `setItems(newItems)` and then calls `setItems(oldItems)`:
|
|
125
|
+
// in that case, `this.newItemsWillBeRendered` flag will be `true` but the actual `items`
|
|
126
|
+
// in state wouldn't have changed due to the first `updateState()` call being overwritten
|
|
127
|
+
// by the second `updateState()` call (that's called "batching state updates" in React).
|
|
128
|
+
if (newItems !== previousItems) {
|
|
129
|
+
const itemsDiff = this.getItemsDiff(previousItems, newItems)
|
|
130
|
+
if (itemsDiff) {
|
|
131
|
+
// The call to `.onPrepend()` must precede the call to `.measureItemHeights()`
|
|
132
|
+
// which is called in `.onRender()`.
|
|
133
|
+
// `this.itemHeights.onPrepend()` updates `firstMeasuredItemIndex`
|
|
134
|
+
// and `lastMeasuredItemIndex` of `this.itemHeights`.
|
|
135
|
+
const { prependedItemsCount } = itemsDiff
|
|
136
|
+
this.itemHeights.onPrepend(prependedItemsCount)
|
|
137
|
+
} else {
|
|
138
|
+
this.itemHeights.reset()
|
|
139
|
+
}
|
|
98
140
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
141
|
+
if (!widthHasChanged) {
|
|
142
|
+
// The call to `this.onNewItemsRendered()` must precede the call to
|
|
143
|
+
// `.measureItemHeights()` which is called in `.onRender()` because
|
|
144
|
+
// `this.onNewItemsRendered()` updates `firstMeasuredItemIndex` and
|
|
145
|
+
// `lastMeasuredItemIndex` of `this.itemHeights` in case of a prepend.
|
|
146
|
+
//
|
|
147
|
+
// If after prepending items the scroll position
|
|
148
|
+
// should be "restored" so that there's no "jump" of content
|
|
149
|
+
// then it means that all previous items have just been rendered
|
|
150
|
+
// in a single pass, and there's no need to update layout again.
|
|
151
|
+
//
|
|
152
|
+
if (onNewItemsRendered.call(this, itemsDiff, newState) !== 'SEAMLESS_PREPEND') {
|
|
153
|
+
layoutUpdateReason = LAYOUT_REASON.ITEMS_CHANGED
|
|
154
|
+
}
|
|
112
155
|
}
|
|
113
156
|
}
|
|
114
157
|
}
|
|
@@ -124,9 +167,11 @@ export default function() {
|
|
|
124
167
|
// item height measurements is required.
|
|
125
168
|
//
|
|
126
169
|
if (
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
170
|
+
(prevState && (
|
|
171
|
+
newState.firstShownItemIndex !== prevState.firstShownItemIndex ||
|
|
172
|
+
newState.lastShownItemIndex !== prevState.lastShownItemIndex ||
|
|
173
|
+
newState.items !== prevState.items
|
|
174
|
+
)) ||
|
|
130
175
|
widthHasChanged
|
|
131
176
|
) {
|
|
132
177
|
const verticalSpacingStateUpdate = this.measureItemHeightsAndSpacing()
|
|
@@ -202,14 +247,14 @@ export default function() {
|
|
|
202
247
|
// See if any items' heights changed while new items were being rendered.
|
|
203
248
|
if (this.itemHeightsThatChangedWhileNewItemsWereBeingRendered) {
|
|
204
249
|
for (const i of Object.keys(this.itemHeightsThatChangedWhileNewItemsWereBeingRendered)) {
|
|
205
|
-
itemHeights[prependedItemsCount +
|
|
250
|
+
itemHeights[prependedItemsCount + Number(i)] = this.itemHeightsThatChangedWhileNewItemsWereBeingRendered[i]
|
|
206
251
|
}
|
|
207
252
|
}
|
|
208
253
|
|
|
209
254
|
// See if any items' states changed while new items were being rendered.
|
|
210
255
|
if (this.itemStatesThatChangedWhileNewItemsWereBeingRendered) {
|
|
211
256
|
for (const i of Object.keys(this.itemStatesThatChangedWhileNewItemsWereBeingRendered)) {
|
|
212
|
-
itemStates[prependedItemsCount +
|
|
257
|
+
itemStates[prependedItemsCount + Number(i)] = this.itemStatesThatChangedWhileNewItemsWereBeingRendered[i]
|
|
213
258
|
}
|
|
214
259
|
}
|
|
215
260
|
|
|
@@ -325,6 +370,9 @@ export default function() {
|
|
|
325
370
|
|
|
326
371
|
// Read `this.firstNonMeasuredItemIndex` flag.
|
|
327
372
|
const nonMeasuredItemsHaveBeenRendered = this.firstNonMeasuredItemIndex !== undefined
|
|
373
|
+
if (nonMeasuredItemsHaveBeenRendered) {
|
|
374
|
+
log('Non-measured item index', this.firstNonMeasuredItemIndex)
|
|
375
|
+
}
|
|
328
376
|
// Reset `this.firstNonMeasuredItemIndex` flag.
|
|
329
377
|
this.firstNonMeasuredItemIndex = undefined
|
|
330
378
|
|
|
@@ -337,8 +385,13 @@ export default function() {
|
|
|
337
385
|
// Reset `this.itemStatesThatChangedWhileNewItemsWereBeingRendered`.
|
|
338
386
|
this.itemStatesThatChangedWhileNewItemsWereBeingRendered = undefined
|
|
339
387
|
|
|
388
|
+
// Reset `this.updateLayoutAfterRenderBecauseItemHeightChanged`.
|
|
389
|
+
const itemHeightHasChanged = this.updateLayoutAfterRenderBecauseItemHeightChanged
|
|
390
|
+
this.updateLayoutAfterRenderBecauseItemHeightChanged = undefined
|
|
391
|
+
|
|
340
392
|
return {
|
|
341
393
|
nonMeasuredItemsHaveBeenRendered,
|
|
394
|
+
itemHeightHasChanged,
|
|
342
395
|
widthHasChanged
|
|
343
396
|
}
|
|
344
397
|
}
|
|
@@ -52,7 +52,13 @@ export default function createStateHelpers({
|
|
|
52
52
|
|
|
53
53
|
this.getState().itemStates[i] = newItemState
|
|
54
54
|
|
|
55
|
-
//
|
|
55
|
+
// If there was a request for `setState()` with new `items`, then the changes
|
|
56
|
+
// to `currentState.itemStates[]` made above would be overwritten when that
|
|
57
|
+
// pending `setState()` call gets applied.
|
|
58
|
+
// To fix that, the updates to current `itemStates[]` are noted in
|
|
59
|
+
// `this.itemStatesThatChangedWhileNewItemsWereBeingRendered` variable.
|
|
60
|
+
// That variable is then checked when the `setState()` call with the new `items`
|
|
61
|
+
// has been updated.
|
|
56
62
|
if (this.newItemsWillBeRendered) {
|
|
57
63
|
if (!this.itemStatesThatChangedWhileNewItemsWereBeingRendered) {
|
|
58
64
|
this.itemStatesThatChangedWhileNewItemsWereBeingRendered = {}
|
|
@@ -78,9 +84,25 @@ export default function createStateHelpers({
|
|
|
78
84
|
}
|
|
79
85
|
this._isSettingNewItems = undefined
|
|
80
86
|
|
|
81
|
-
|
|
87
|
+
this.waitingForRender = true
|
|
88
|
+
|
|
89
|
+
// Store previous `state`.
|
|
82
90
|
this.previousState = this.getState()
|
|
83
|
-
|
|
91
|
+
|
|
92
|
+
// If it's the first call to `this.updateState()` then initialize
|
|
93
|
+
// the most recent `setState()` value to be the current state.
|
|
94
|
+
if (!this.mostRecentSetStateValue) {
|
|
95
|
+
this.mostRecentSetStateValue = this.getState()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Accumulates all "pending" state updates until they have been applied.
|
|
99
|
+
this.mostRecentSetStateValue = {
|
|
100
|
+
...this.mostRecentSetStateValue,
|
|
101
|
+
...stateUpdate
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Update `state`.
|
|
105
|
+
this._setState(this.mostRecentSetStateValue, stateUpdate)
|
|
84
106
|
}
|
|
85
107
|
|
|
86
108
|
this.getInitialState = () => {
|
|
@@ -92,6 +114,7 @@ export default function createStateHelpers({
|
|
|
92
114
|
|
|
93
115
|
this.useState = ({
|
|
94
116
|
getState,
|
|
117
|
+
setState,
|
|
95
118
|
updateState
|
|
96
119
|
}) => {
|
|
97
120
|
if (this._isActive) {
|
|
@@ -103,17 +126,28 @@ export default function createStateHelpers({
|
|
|
103
126
|
}
|
|
104
127
|
|
|
105
128
|
if (render) {
|
|
106
|
-
throw new Error('[virtual-scroller] Creating a `VirtualScroller` class instance with a `render()` parameter
|
|
129
|
+
throw new Error('[virtual-scroller] Creating a `VirtualScroller` class instance with a `render()` parameter implies using the default (internal) state storage')
|
|
107
130
|
}
|
|
108
131
|
|
|
109
|
-
if (
|
|
110
|
-
throw new Error('[virtual-scroller] When using a custom state storage, one must supply
|
|
132
|
+
if (setState && updateState) {
|
|
133
|
+
throw new Error('[virtual-scroller] When using a custom state storage, one must supply either `setState()` or `updateState()` function but not both')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!getState || !(setState || updateState)) {
|
|
137
|
+
throw new Error('[virtual-scroller] When using a custom state storage, one must supply both `getState()` and `setState()`/`updateState()` functions')
|
|
111
138
|
}
|
|
112
139
|
|
|
113
140
|
this._usesCustomStateStorage = true
|
|
114
141
|
|
|
115
142
|
this._getState = getState
|
|
116
|
-
|
|
143
|
+
|
|
144
|
+
this._setState = (newState, stateUpdate) => {
|
|
145
|
+
if (setState) {
|
|
146
|
+
setState(newState)
|
|
147
|
+
} else {
|
|
148
|
+
updateState(stateUpdate)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
117
151
|
}
|
|
118
152
|
|
|
119
153
|
this.useDefaultStateStorage = () => {
|
|
@@ -121,9 +155,9 @@ export default function createStateHelpers({
|
|
|
121
155
|
throw new Error('[virtual-scroller] When using the default (internal) state management, one must supply a `render(state, prevState)` function parameter')
|
|
122
156
|
}
|
|
123
157
|
|
|
124
|
-
// Create default `getState()`/`
|
|
158
|
+
// Create default `getState()`/`setState()` functions.
|
|
125
159
|
this._getState = defaultGetState.bind(this)
|
|
126
|
-
this.
|
|
160
|
+
this._setState = defaultSetState.bind(this)
|
|
127
161
|
|
|
128
162
|
// When `state` is stored externally, a developer is responsible for
|
|
129
163
|
// initializing it with the initial value.
|
|
@@ -140,17 +174,20 @@ export default function createStateHelpers({
|
|
|
140
174
|
this.state = newState
|
|
141
175
|
}
|
|
142
176
|
|
|
143
|
-
function
|
|
144
|
-
// Because
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
this.state
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
177
|
+
function defaultSetState(newState, stateUpdate) {
|
|
178
|
+
// // Because the default state updates are "synchronous" (immediate),
|
|
179
|
+
// // the `...stateUpdate` could be applied over `...this.state`,
|
|
180
|
+
// // and no state updates would be lost.
|
|
181
|
+
// // But if it was "asynchronous" (not immediate), then `...this.state`
|
|
182
|
+
// // wouldn't work in all cases, because it could be stale in cases
|
|
183
|
+
// // when more than a single `setState()` call is made before
|
|
184
|
+
// // the state actually updates, making some properties of `this.state` stale.
|
|
185
|
+
// this.state = {
|
|
186
|
+
// ...this.state,
|
|
187
|
+
// ...stateUpdate
|
|
188
|
+
// }
|
|
189
|
+
|
|
190
|
+
this.state = newState
|
|
154
191
|
|
|
155
192
|
render(this.state, this.previousState)
|
|
156
193
|
|
|
@@ -8,32 +8,21 @@ import useInstanceMethods from './useInstanceMethods.js'
|
|
|
8
8
|
import useItemKeys from './useItemKeys.js'
|
|
9
9
|
import useSetItemState from './useSetItemState.js'
|
|
10
10
|
import useOnItemHeightDidChange from './useOnItemHeightDidChange.js'
|
|
11
|
-
import
|
|
12
|
-
import
|
|
11
|
+
import useSetNewItemsOnItemsPropertyChange from './useSetNewItemsOnItemsPropertyChange.js'
|
|
12
|
+
import useUpdateItemKeysOnItemsChange from './useUpdateItemKeysOnItemsChange.js'
|
|
13
13
|
import useClassName from './useClassName.js'
|
|
14
14
|
import useStyle from './useStyle.js'
|
|
15
15
|
|
|
16
|
-
// When `items` property changes
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// Another reason for using this feature is:
|
|
28
|
-
//
|
|
29
|
-
// Since `useHandleItemsPropertyChange()` runs at render time
|
|
30
|
-
// and not after the render has finished (not in an "effect"),
|
|
31
|
-
// if the state update was done "conventionally" (by calling `_setNewState()`),
|
|
32
|
-
// React would throw an error about updating state during render.
|
|
33
|
-
// No one knows what the original error message was.
|
|
34
|
-
// Perhaps it's no longer relevant in newer versions of React.
|
|
35
|
-
//
|
|
36
|
-
const USE_ITEMS_UPDATE_NO_SECOND_RENDER_OPTIMIZATION = true
|
|
16
|
+
// When `items` property changes:
|
|
17
|
+
// * A new `items` property is supplied to the React component.
|
|
18
|
+
// * The React component re-renders itself.
|
|
19
|
+
// * `useSetNewItemsOnItemsPropertyChange()` hook is run.
|
|
20
|
+
// * `useSetNewItemsOnItemsPropertyChange()` hook detects that the `items` property
|
|
21
|
+
// has changed and calls `VirtualScroller.setItems(items)`.
|
|
22
|
+
// * `VirtualScroller.setItems(items)` calls `VirtualScroller.setState()`.
|
|
23
|
+
// * `VirtualScroller.setState()` calls the `setState()` function.
|
|
24
|
+
// * The `setState()` function calls a setter from a `useState()` hook.
|
|
25
|
+
// * The React component re-renders itself the second time.
|
|
37
26
|
|
|
38
27
|
function VirtualScroller({
|
|
39
28
|
as: AsComponent,
|
|
@@ -113,20 +102,19 @@ function VirtualScroller({
|
|
|
113
102
|
// This way, React will re-render the component on every state update.
|
|
114
103
|
const {
|
|
115
104
|
getState,
|
|
116
|
-
|
|
117
|
-
|
|
105
|
+
setState,
|
|
106
|
+
stateToRender
|
|
118
107
|
} = useState({
|
|
119
108
|
initialState: _initialState,
|
|
120
109
|
onRender: virtualScroller.onRender,
|
|
121
|
-
itemsProperty
|
|
122
|
-
USE_ITEMS_UPDATE_NO_SECOND_RENDER_OPTIMIZATION
|
|
110
|
+
itemsProperty
|
|
123
111
|
})
|
|
124
112
|
|
|
125
113
|
// Use custom (external) state storage in the `VirtualScroller`.
|
|
126
114
|
useMemo(() => {
|
|
127
115
|
virtualScroller.useState({
|
|
128
116
|
getState,
|
|
129
|
-
|
|
117
|
+
setState
|
|
130
118
|
})
|
|
131
119
|
}, [])
|
|
132
120
|
|
|
@@ -138,6 +126,7 @@ function VirtualScroller({
|
|
|
138
126
|
// "reuse" `itemComponent`s in cases when `items` are changed.
|
|
139
127
|
const {
|
|
140
128
|
getItemKey,
|
|
129
|
+
usesAutogeneratedItemKeys,
|
|
141
130
|
updateItemKeysForNewItems
|
|
142
131
|
} = useItemKeys({
|
|
143
132
|
getItemId
|
|
@@ -158,19 +147,18 @@ function VirtualScroller({
|
|
|
158
147
|
})
|
|
159
148
|
|
|
160
149
|
// Calls `.setItems()` if `items` property has changed.
|
|
161
|
-
|
|
150
|
+
useSetNewItemsOnItemsPropertyChange(itemsProperty, {
|
|
162
151
|
virtualScroller,
|
|
163
152
|
// `preserveScrollPosition` property name is deprecated,
|
|
164
153
|
// use `preserveScrollPositionOnPrependItems` property instead.
|
|
165
154
|
preserveScrollPosition,
|
|
166
|
-
preserveScrollPositionOnPrependItems
|
|
167
|
-
nextItems: getNextState().items
|
|
155
|
+
preserveScrollPositionOnPrependItems
|
|
168
156
|
})
|
|
169
157
|
|
|
170
158
|
// Updates `key`s if item indexes have changed.
|
|
171
|
-
|
|
159
|
+
useUpdateItemKeysOnItemsChange(stateToRender.items, {
|
|
172
160
|
virtualScroller,
|
|
173
|
-
|
|
161
|
+
usesAutogeneratedItemKeys,
|
|
174
162
|
updateItemKeysForNewItems
|
|
175
163
|
})
|
|
176
164
|
|
|
@@ -209,7 +197,7 @@ function VirtualScroller({
|
|
|
209
197
|
|
|
210
198
|
const style = useStyle({
|
|
211
199
|
tbody,
|
|
212
|
-
|
|
200
|
+
state: stateToRender
|
|
213
201
|
})
|
|
214
202
|
|
|
215
203
|
const {
|
|
@@ -217,7 +205,7 @@ function VirtualScroller({
|
|
|
217
205
|
itemStates,
|
|
218
206
|
firstShownItemIndex,
|
|
219
207
|
lastShownItemIndex
|
|
220
|
-
} =
|
|
208
|
+
} = stateToRender
|
|
221
209
|
|
|
222
210
|
return (
|
|
223
211
|
<AsComponent
|