11
11
*/
12
12
13
13
import { BaseCollection , CollectionNode , Mutable } from './BaseCollection' ;
14
- import { CSSProperties , ForwardedRef , ReactNode } from 'react' ;
14
+ import { CSSProperties , ForwardedRef , ReactElement , ReactNode } from 'react' ;
15
15
import { Node } from '@react-types/shared' ;
16
16
17
17
// This Collection implementation is perhaps a little unusual. It works by rendering the React tree into a
@@ -38,6 +38,7 @@ export class BaseNode<T> {
38
38
private _previousSibling : ElementNode < T > | null = null ;
39
39
private _nextSibling : ElementNode < T > | null = null ;
40
40
private _parentNode : BaseNode < T > | null = null ;
41
+ private _minInvalidChildIndex : ElementNode < T > | null = null ;
41
42
ownerDocument : Document < T , any > ;
42
43
43
44
constructor ( ownerDocument : Document < T , any > ) {
@@ -101,6 +102,21 @@ export class BaseNode<T> {
101
102
return this . parentNode ?. isConnected || false ;
102
103
}
103
104
105
+ private invalidateChildIndices ( child : ElementNode < T > ) : void {
106
+ if ( this . _minInvalidChildIndex == null || child . index < this . _minInvalidChildIndex . index ) {
107
+ this . _minInvalidChildIndex = child ;
108
+ }
109
+ }
110
+
111
+ updateChildIndices ( ) : void {
112
+ let node = this . _minInvalidChildIndex ;
113
+ while ( node ) {
114
+ node . index = node . previousSibling ? node . previousSibling . index + 1 : 0 ;
115
+ node = node . nextSibling ;
116
+ }
117
+ this . _minInvalidChildIndex = null ;
118
+ }
119
+
104
120
appendChild ( child : ElementNode < T > ) : void {
105
121
this . ownerDocument . startTransaction ( ) ;
106
122
if ( child . parentNode ) {
@@ -158,11 +174,7 @@ export class BaseNode<T> {
158
174
referenceNode . previousSibling = newNode ;
159
175
newNode . parentNode = referenceNode . parentNode ;
160
176
161
- let node : ElementNode < T > | null = referenceNode ;
162
- while ( node ) {
163
- node . index ++ ;
164
- node = node . nextSibling ;
165
- }
177
+ this . invalidateChildIndices ( referenceNode ) ;
166
178
167
179
if ( newNode . hasSetProps ) {
168
180
this . ownerDocument . addNode ( newNode ) ;
@@ -178,13 +190,9 @@ export class BaseNode<T> {
178
190
}
179
191
180
192
this . ownerDocument . startTransaction ( ) ;
181
- let node = child . nextSibling ;
182
- while ( node ) {
183
- node . index -- ;
184
- node = node . nextSibling ;
185
- }
186
-
193
+
187
194
if ( child . nextSibling ) {
195
+ this . invalidateChildIndices ( child . nextSibling ) ;
188
196
child . nextSibling . previousSibling = child . previousSibling ;
189
197
}
190
198
@@ -272,7 +280,7 @@ export class ElementNode<T> extends BaseNode<T> {
272
280
}
273
281
}
274
282
275
- setProps < E extends Element > ( obj : { [ key : string ] : any } , ref : ForwardedRef < E > , rendered ?: ReactNode , render ?: ( node : Node < T > ) => ReactNode ) : void {
283
+ setProps < E extends Element > ( obj : { [ key : string ] : any } , ref : ForwardedRef < E > , rendered ?: ReactNode , render ?: ( node : Node < T > ) => ReactElement ) : void {
276
284
let node = this . ownerDocument . getMutableNode ( this ) ;
277
285
let { value, textValue, id, ...props } = obj ;
278
286
props . ref = ref ;
@@ -330,6 +338,8 @@ export class Document<T, C extends BaseCollection<T> = BaseCollection<T>> extend
330
338
private mutatedNodes : Set < ElementNode < T > > = new Set ( ) ;
331
339
private subscriptions : Set < ( ) => void > = new Set ( ) ;
332
340
private transactionCount = 0 ;
341
+ private queuedRender = false ;
342
+ private inSubscription = false ;
333
343
334
344
constructor ( collection : C ) {
335
345
// @ts -ignore
@@ -412,10 +422,22 @@ export class Document<T, C extends BaseCollection<T> = BaseCollection<T>> extend
412
422
}
413
423
414
424
this . updateCollection ( ) ;
425
+
426
+ // Reset queuedRender to false when getCollection is called during render.
427
+ if ( ! this . inSubscription ) {
428
+ this . queuedRender = false ;
429
+ }
430
+
415
431
return this . collection ;
416
432
}
417
433
418
434
updateCollection ( ) : void {
435
+ // First, update the indices of dirty element children.
436
+ for ( let element of this . dirtyNodes ) {
437
+ element . updateChildIndices ( ) ;
438
+ }
439
+
440
+ // Next, update dirty collection nodes.
419
441
for ( let element of this . dirtyNodes ) {
420
442
if ( element instanceof ElementNode && element . isConnected ) {
421
443
element . updateNode ( ) ;
@@ -424,6 +446,7 @@ export class Document<T, C extends BaseCollection<T> = BaseCollection<T>> extend
424
446
425
447
this . dirtyNodes . clear ( ) ;
426
448
449
+ // Finally, update the collection.
427
450
if ( this . mutatedNodes . size || this . collectionMutated ) {
428
451
let collection = this . getMutableCollection ( ) ;
429
452
for ( let element of this . mutatedNodes ) {
@@ -442,13 +465,21 @@ export class Document<T, C extends BaseCollection<T> = BaseCollection<T>> extend
442
465
queueUpdate ( ) : void {
443
466
// Don't emit any updates if there is a transaction in progress.
444
467
// queueUpdate should be called again after the transaction.
445
- if ( this . dirtyNodes . size === 0 || this . transactionCount > 0 ) {
468
+ if ( this . dirtyNodes . size === 0 || this . transactionCount > 0 || this . queuedRender ) {
446
469
return ;
447
470
}
448
471
472
+ // Only trigger subscriptions once during an update, when the first item changes.
473
+ // React's useSyncExternalStore will call getCollection immediately, to check whether the snapshot changed.
474
+ // If so, React will queue a render to happen after the current commit to our fake DOM finishes.
475
+ // We track whether getCollection is called in a subscription, and once it is called during render,
476
+ // we reset queuedRender back to false.
477
+ this . queuedRender = true ;
478
+ this . inSubscription = true ;
449
479
for ( let fn of this . subscriptions ) {
450
480
fn ( ) ;
451
481
}
482
+ this . inSubscription = false ;
452
483
}
453
484
454
485
subscribe ( fn : ( ) => void ) {
0 commit comments