1
-/**
2
- * Copyright (c) Meta Platforms, Inc. and affiliates.
3
- *
4
- * This source code is licensed under the MIT license found in the
5
- * LICENSE file in the root directory of this source tree.
6
- *
7
- * @emails react-core
8
- * @jest-environment node
9
- */
10
-
11
-/* eslint-disable no-func-assign */
12
-
13
-'use strict';
14
-
15
-let React;
16
-let ReactFeatureFlags;
17
-let ReactNoop;
18
-let Scheduler;
19
-let act;
20
-let createMutableSource;
21
-let useMutableSource;
22
-let waitFor;
23
-let waitForAll;
24
-let assertLog;
25
-let waitForPaint;
26
-
27
-function loadModules() {
28
- jest.resetModules();
29
- jest.useFakeTimers();
30
-
31
- ReactFeatureFlags = require('shared/ReactFeatureFlags');
32
- ReactFeatureFlags.enableProfilerTimer = true;
33
-
34
- React = require('react');
35
- ReactNoop = require('react-noop-renderer');
36
- Scheduler = require('scheduler');
37
- act = require('internal-test-utils').act;
38
-
39
- const InternalTestUtils = require('internal-test-utils');
40
- waitFor = InternalTestUtils.waitFor;
41
- waitForAll = InternalTestUtils.waitForAll;
42
- waitForPaint = InternalTestUtils.waitForPaint;
43
- assertLog = InternalTestUtils.assertLog;
44
-
45
- // Stable entrypoints export with "unstable_" prefix.
46
- createMutableSource =
47
- React.createMutableSource || React.unstable_createMutableSource;
48
- useMutableSource = React.useMutableSource || React.unstable_useMutableSource;
49
-}
50
-
51
-describe('useMutableSource', () => {
52
- const defaultGetSnapshot = source => source.value;
53
- const defaultSubscribe = (source, callback) => source.subscribe(callback);
54
-
55
- function createComplexSource(initialValueA, initialValueB) {
56
- const callbacksA = [];
57
- const callbacksB = [];
58
- let revision = 0;
59
- let valueA = initialValueA;
60
- let valueB = initialValueB;
61
-
62
- const subscribeHelper = (callbacks, callback) => {
63
- if (callbacks.indexOf(callback) < 0) {
64
- callbacks.push(callback);
65
- }
66
- return () => {
67
- const index = callbacks.indexOf(callback);
68
- if (index >= 0) {
69
- callbacks.splice(index, 1);
70
- }
71
- };
72
- };
73
-
74
- return {
75
- subscribeA(callback) {
76
- return subscribeHelper(callbacksA, callback);
77
- },
78
- subscribeB(callback) {
79
- return subscribeHelper(callbacksB, callback);
80
- },
81
-
82
- get listenerCountA() {
83
- return callbacksA.length;
84
- },
85
- get listenerCountB() {
86
- return callbacksB.length;
87
- },
88
-
89
- set valueA(newValue) {
90
- revision++;
91
- valueA = newValue;
92
- callbacksA.forEach(callback => callback());
93
- },
94
- get valueA() {
95
- return valueA;
96
- },
97
-
98
- set valueB(newValue) {
99
- revision++;
100
- valueB = newValue;
101
- callbacksB.forEach(callback => callback());
102
- },
103
- get valueB() {
104
- return valueB;
105
- },
106
-
107
- get version() {
108
- return revision;
109
- },
110
- };
111
- }
112
-
113
- function createSource(initialValue) {
114
- const callbacks = [];
115
- let revision = 0;
116
- let value = initialValue;
117
- return {
118
- subscribe(callback) {
119
- if (callbacks.indexOf(callback) < 0) {
120
- callbacks.push(callback);
121
- }
122
- return () => {
123
- const index = callbacks.indexOf(callback);
124
- if (index >= 0) {
125
- callbacks.splice(index, 1);
126
- }
127
- };
128
- },
129
- get listenerCount() {
130
- return callbacks.length;
131
- },
132
- set value(newValue) {
133
- revision++;
134
- value = newValue;
135
- callbacks.forEach(callback => callback());
136
- },
137
- get value() {
138
- return value;
139
- },
140
- get version() {
141
- return revision;
142
- },
143
- };
144
- }
145
-
146
- function Component({getSnapshot, label, mutableSource, subscribe}) {
147
- const snapshot = useMutableSource(mutableSource, getSnapshot, subscribe);
148
- Scheduler.log(`${label}:${snapshot}`);
149
- return <div>{`${label}:${snapshot}`}</div>;
150
- }
151
-
152
- beforeEach(loadModules);
153
-
154
- // @gate enableUseMutableSource
155
- it('should subscribe to a source and schedule updates when it changes', async () => {
156
- const source = createSource('one');
157
- const mutableSource = createMutableSource(source, param => param.version);
158
-
159
- await act(async () => {
160
- ReactNoop.renderToRootWithID(
161
- <>
162
- <Component
163
- label="a"
164
- getSnapshot={defaultGetSnapshot}
165
- mutableSource={mutableSource}
166
- subscribe={defaultSubscribe}
167
- />
168
- <Component
169
- label="b"
170
- getSnapshot={defaultGetSnapshot}
171
- mutableSource={mutableSource}
172
- subscribe={defaultSubscribe}
173
- />
174
- </>,
175
- 'root',
176
- () => Scheduler.log('Sync effect'),
177
- );
178
- await waitFor(['a:one', 'b:one', 'Sync effect']);
179
-
180
- // Subscriptions should be passive
181
- expect(source.listenerCount).toBe(0);
182
- ReactNoop.flushPassiveEffects();
183
- expect(source.listenerCount).toBe(2);
184
-
185
- // Changing values should schedule an update with React
186
- source.value = 'two';
187
- await waitFor(['a:two', 'b:two']);
188
-
189
- // Unmounting a component should remove its subscription.
190
- ReactNoop.renderToRootWithID(
191
- <>
192
- <Component
193
- label="a"
194
- getSnapshot={defaultGetSnapshot}
195
- mutableSource={mutableSource}
196
- subscribe={defaultSubscribe}
197
- />
198
- </>,
199
- 'root',
200
- () => Scheduler.log('Sync effect'),
201
- );
202
- await waitForAll(['a:two', 'Sync effect']);
203
- ReactNoop.flushPassiveEffects();
204
- expect(source.listenerCount).toBe(1);
205
-
206
- // Unmounting a root should remove the remaining event listeners
207
- ReactNoop.unmountRootWithID('root');
208
- await waitForAll([]);
209
- ReactNoop.flushPassiveEffects();
210
- expect(source.listenerCount).toBe(0);
211
-
212
- // Changes to source should not trigger an updates or warnings.
213
- source.value = 'three';
214
- await waitForAll([]);
215
- });
216
- });
217
-
218
- // @gate enableUseMutableSource
219
- it('should restart work if a new source is mutated during render', async () => {
220
- const source = createSource('one');
221
- const mutableSource = createMutableSource(source, param => param.version);
222
-
223
- await act(async () => {
224
- React.startTransition(() => {
225
- ReactNoop.render(
226
- <>
227
- <Component
228
- label="a"
229
- getSnapshot={defaultGetSnapshot}
230
- mutableSource={mutableSource}
231
- subscribe={defaultSubscribe}
232
- />
233
- <Component
234
- label="b"
235
- getSnapshot={defaultGetSnapshot}
236
- mutableSource={mutableSource}
237
- subscribe={defaultSubscribe}
238
- />
239
- </>,
240
- () => Scheduler.log('Sync effect'),
241
- );
242
- });
243
-
244
- // Do enough work to read from one component
245
- await waitFor(['a:one']);
246
-
247
- // Mutate source before continuing work
248
- source.value = 'two';
249
-
250
- // Render work should restart and the updated value should be used
251
- await waitForAll(['a:two', 'b:two', 'Sync effect']);
252
- });
253
- });
254
-
255
- // @gate enableUseMutableSource
256
- it('should schedule an update if a new source is mutated between render and commit (subscription)', async () => {
257
- const source = createSource('one');
258
- const mutableSource = createMutableSource(source, param => param.version);
259
-
260
- await act(async () => {
261
- ReactNoop.render(
262
- <>
263
- <Component
264
- label="a"
265
- getSnapshot={defaultGetSnapshot}
266
- mutableSource={mutableSource}
267
- subscribe={defaultSubscribe}
268
- />
269
- <Component
270
- label="b"
271
- getSnapshot={defaultGetSnapshot}
272
- mutableSource={mutableSource}
273
- subscribe={defaultSubscribe}
274
- />
275
- </>,
276
- () => Scheduler.log('Sync effect'),
277
- );
278
-
279
- // Finish rendering
280
- await waitFor(['a:one', 'b:one', 'Sync effect']);
281
-
282
- // Mutate source before subscriptions are attached
283
- expect(source.listenerCount).toBe(0);
284
- source.value = 'two';
285
-
286
- // Mutation should be detected, and a new render should be scheduled
287
- await waitForAll(['a:two', 'b:two']);
288
- });
289
- });
290
-
291
- // @gate enableUseMutableSource
292
- it('should unsubscribe and resubscribe if a new source is used', async () => {
293
- const sourceA = createSource('a-one');
294
- const mutableSourceA = createMutableSource(
295
- sourceA,
296
- param => param.versionA,
297
- );
298
-
299
- const sourceB = createSource('b-one');
300
- const mutableSourceB = createMutableSource(
301
- sourceB,
302
- param => param.versionB,
303
- );
304
-
305
- await act(async () => {
306
- ReactNoop.render(
307
- <Component
308
- label="only"
309
- getSnapshot={defaultGetSnapshot}
310
- mutableSource={mutableSourceA}
311
- subscribe={defaultSubscribe}
312
- />,
313
- () => Scheduler.log('Sync effect'),
314
- );
315
- await waitForAll(['only:a-one', 'Sync effect']);
316
- ReactNoop.flushPassiveEffects();
317
- expect(sourceA.listenerCount).toBe(1);
318
-
319
- // Changing values should schedule an update with React
320
- sourceA.value = 'a-two';
321
- await waitForAll(['only:a-two']);
322
-
323
- // If we re-render with a new source, the old one should be unsubscribed.
324
- ReactNoop.render(
325
- <Component
326
- label="only"
327
- getSnapshot={defaultGetSnapshot}
328
- mutableSource={mutableSourceB}
329
- subscribe={defaultSubscribe}
330
- />,
331
- () => Scheduler.log('Sync effect'),
332
- );
333
- await waitForAll(['only:b-one', 'Sync effect']);
334
- ReactNoop.flushPassiveEffects();
335
- expect(sourceA.listenerCount).toBe(0);
336
- expect(sourceB.listenerCount).toBe(1);
337
-
338
- // Changing to original source should not schedule updates with React
339
- sourceA.value = 'a-three';
340
- await waitForAll([]);
341
-
342
- // Changing new source value should schedule an update with React
343
- sourceB.value = 'b-two';
344
- await waitForAll(['only:b-two']);
345
- });
346
- });
347
-
348
- // @gate enableUseMutableSource
349
- it('should unsubscribe and resubscribe if a new subscribe function is provided', async () => {
350
- const source = createSource('a-one');
351
- const mutableSource = createMutableSource(source, param => param.version);
352
-
353
- const unsubscribeA = jest.fn();
354
- const subscribeA = jest.fn(s => {
355
- const unsubscribe = defaultSubscribe(s);
356
- return () => {
357
- unsubscribe();
358
- unsubscribeA();
359
- };
360
- });
361
- const unsubscribeB = jest.fn();
362
- const subscribeB = jest.fn(s => {
363
- const unsubscribe = defaultSubscribe(s);
364
- return () => {
365
- unsubscribe();
366
- unsubscribeB();
367
- };
368
- });
369
-
370
- await act(async () => {
371
- ReactNoop.renderToRootWithID(
372
- <Component
373
- label="only"
374
- getSnapshot={defaultGetSnapshot}
375
- mutableSource={mutableSource}
376
- subscribe={subscribeA}
377
- />,
378
- 'root',
379
- () => Scheduler.log('Sync effect'),
380
- );
381
- await waitForAll(['only:a-one', 'Sync effect']);
382
- ReactNoop.flushPassiveEffects();
383
- expect(source.listenerCount).toBe(1);
384
- expect(subscribeA).toHaveBeenCalledTimes(1);
385
-
386
- // If we re-render with a new subscription function,
387
- // the old unsubscribe function should be called.
388
- ReactNoop.renderToRootWithID(
389
- <Component
390
- label="only"
391
- getSnapshot={defaultGetSnapshot}
392
- mutableSource={mutableSource}
393
- subscribe={subscribeB}
394
- />,
395
- 'root',
396
- () => Scheduler.log('Sync effect'),
397
- );
398
- await waitForAll(['only:a-one', 'Sync effect']);
399
- ReactNoop.flushPassiveEffects();
400
- expect(source.listenerCount).toBe(1);
401
- expect(unsubscribeA).toHaveBeenCalledTimes(1);
402
- expect(subscribeB).toHaveBeenCalledTimes(1);
403
-
404
- // Unmounting should call the newer unsubscribe.
405
- ReactNoop.unmountRootWithID('root');
406
- await waitForAll([]);
407
- ReactNoop.flushPassiveEffects();
408
- expect(source.listenerCount).toBe(0);
409
- expect(unsubscribeB).toHaveBeenCalledTimes(1);
410
- });
411
- });
412
-
413
- // @gate enableUseMutableSource
414
- it('should re-use previously read snapshot value when reading is unsafe', async () => {
415
- const source = createSource('one');
416
- const mutableSource = createMutableSource(source, param => param.version);
417
-
418
- await act(async () => {
419
- ReactNoop.render(
420
- <>
421
- <Component
422
- label="a"
423
- getSnapshot={defaultGetSnapshot}
424
- mutableSource={mutableSource}
425
- subscribe={defaultSubscribe}
426
- />
427
- <Component
428
- label="b"
429
- getSnapshot={defaultGetSnapshot}
430
- mutableSource={mutableSource}
431
- subscribe={defaultSubscribe}
432
- />
433
- </>,
434
- () => Scheduler.log('Sync effect'),
435
- );
436
- await waitForAll(['a:one', 'b:one', 'Sync effect']);
437
-
438
- // Changing values should schedule an update with React.
439
- // Start working on this update but don't finish it.
440
- React.startTransition(() => {
441
- source.value = 'two';
442
- });
443
- await waitFor(['a:two']);
444
-
445
- // Re-renders that occur before the update is processed
446
- // should reuse snapshot so long as the config has not changed
447
- ReactNoop.flushSync(() => {
448
- ReactNoop.render(
449
- <>
450
- <Component
451
- label="a"
452
- getSnapshot={defaultGetSnapshot}
453
- mutableSource={mutableSource}
454
- subscribe={defaultSubscribe}
455
- />
456
- <Component
457
- label="b"
458
- getSnapshot={defaultGetSnapshot}
459
- mutableSource={mutableSource}
460
- subscribe={defaultSubscribe}
461
- />
462
- </>,
463
- () => Scheduler.log('Sync effect'),
464
- );
465
- });
466
- assertLog(['a:one', 'b:one', 'Sync effect']);
467
-
468
- await waitForAll(['a:two', 'b:two']);
469
- });
470
- });
471
-
472
- // @gate enableUseMutableSource
473
- it('should read from source on newly mounted subtree if no pending updates are scheduled for source', async () => {
474
- const source = createSource('one');
475
- const mutableSource = createMutableSource(source, param => param.version);
476
-
477
- await act(async () => {
478
- ReactNoop.render(
479
- <>
480
- <Component
481
- label="a"
482
- getSnapshot={defaultGetSnapshot}
483
- mutableSource={mutableSource}
484
- subscribe={defaultSubscribe}
485
- />
486
- </>,
487
- () => Scheduler.log('Sync effect'),
488
- );
489
- await waitForAll(['a:one', 'Sync effect']);
490
-
491
- ReactNoop.render(
492
- <>
493
- <Component
494
- label="a"
495
- getSnapshot={defaultGetSnapshot}
496
- mutableSource={mutableSource}
497
- subscribe={defaultSubscribe}
498
- />
499
- <Component
500
- label="b"
501
- getSnapshot={defaultGetSnapshot}
502
- mutableSource={mutableSource}
503
- subscribe={defaultSubscribe}
504
- />
505
- </>,
506
- () => Scheduler.log('Sync effect'),
507
- );
508
- await waitForAll(['a:one', 'b:one', 'Sync effect']);
509
- });
510
- });
511
-
512
- // @gate enableUseMutableSource
513
- it('should throw and restart render if source and snapshot are unavailable during an update', async () => {
514
- const source = createSource('one');
515
- const mutableSource = createMutableSource(source, param => param.version);
516
-
517
- await act(async () => {
518
- ReactNoop.render(
519
- <>
520
- <Component
521
- label="a"
522
- getSnapshot={defaultGetSnapshot}
523
- mutableSource={mutableSource}
524
- subscribe={defaultSubscribe}
525
- />
526
- <Component
527
- label="b"
528
- getSnapshot={defaultGetSnapshot}
529
- mutableSource={mutableSource}
530
- subscribe={defaultSubscribe}
531
- />
532
- </>,
533
- () => Scheduler.log('Sync effect'),
534
- );
535
- await waitForAll(['a:one', 'b:one', 'Sync effect']);
536
- ReactNoop.flushPassiveEffects();
537
-
538
- // Changing values should schedule an update with React.
539
- ReactNoop.idleUpdates(() => {
540
- source.value = 'two';
541
- });
542
-
543
- // Start working on this update but don't finish it.
544
- await waitFor(['a:two']);
545
-
546
- const newGetSnapshot = s => 'new:' + defaultGetSnapshot(s);
547
-
548
- // Force a higher priority render with a new config.
549
- // This should signal that the snapshot is not safe and trigger a full re-render.
550
- ReactNoop.flushSync(() => {
551
- ReactNoop.render(
552
- <>
553
- <Component
554
- label="a"
555
- getSnapshot={newGetSnapshot}
556
- mutableSource={mutableSource}
557
- subscribe={defaultSubscribe}
558
- />
559
- <Component
560
- label="b"
561
- getSnapshot={newGetSnapshot}
562
- mutableSource={mutableSource}
563
- subscribe={defaultSubscribe}
564
- />
565
- </>,
566
- () => Scheduler.log('Sync effect'),
567
- );
568
- });
569
- assertLog(['a:new:two', 'b:new:two', 'Sync effect']);
570
- });
571
- });
572
-
573
- // @gate enableUseMutableSource
574
- it('should throw and restart render if source and snapshot are unavailable during a sync update', async () => {
575
- const source = createSource('one');
576
- const mutableSource = createMutableSource(source, param => param.version);
577
-
578
- await act(async () => {
579
- ReactNoop.render(
580
- <>
581
- <Component
582
- label="a"
583
- getSnapshot={defaultGetSnapshot}
584
- mutableSource={mutableSource}
585
- subscribe={defaultSubscribe}
586
- />
587
- <Component
588
- label="b"
589
- getSnapshot={defaultGetSnapshot}
590
- mutableSource={mutableSource}
591
- subscribe={defaultSubscribe}
592
- />
593
- </>,
594
- () => Scheduler.log('Sync effect'),
595
- );
596
- await waitForAll(['a:one', 'b:one', 'Sync effect']);
597
- ReactNoop.flushPassiveEffects();
598
-
599
- // Changing values should schedule an update with React.
600
- ReactNoop.idleUpdates(() => {
601
- source.value = 'two';
602
- });
603
-
604
- // Start working on this update but don't finish it.
605
- await waitFor(['a:two']);
606
-
607
- const newGetSnapshot = s => 'new:' + defaultGetSnapshot(s);
608
-
609
- // Force a higher priority render with a new config.
610
- // This should signal that the snapshot is not safe and trigger a full re-render.
611
- ReactNoop.flushSync(() => {
612
- ReactNoop.render(
613
- <>
614
- <Component
615
- label="a"
616
- getSnapshot={newGetSnapshot}
617
- mutableSource={mutableSource}
618
- subscribe={defaultSubscribe}
619
- />
620
- <Component
621
- label="b"
622
- getSnapshot={newGetSnapshot}
623
- mutableSource={mutableSource}
624
- subscribe={defaultSubscribe}
625
- />
626
- </>,
627
- () => Scheduler.log('Sync effect'),
628
- );
629
- });
630
- assertLog(['a:new:two', 'b:new:two', 'Sync effect']);
631
- });
632
- });
633
-
634
- // @gate enableUseMutableSource
635
- it('should only update components whose subscriptions fire', async () => {
636
- const source = createComplexSource('a:one', 'b:one');
637
- const mutableSource = createMutableSource(source, param => param.version);
638
-
639
- // Subscribe to part of the store.
640
- const getSnapshotA = s => s.valueA;
641
- const subscribeA = (s, callback) => s.subscribeA(callback);
642
- const getSnapshotB = s => s.valueB;
643
- const subscribeB = (s, callback) => s.subscribeB(callback);
644
-
645
- await act(async () => {
646
- ReactNoop.render(
647
- <>
648
- <Component
649
- label="a"
650
- getSnapshot={getSnapshotA}
651
- mutableSource={mutableSource}
652
- subscribe={subscribeA}
653
- />
654
- <Component
655
- label="b"
656
- getSnapshot={getSnapshotB}
657
- mutableSource={mutableSource}
658
- subscribe={subscribeB}
659
- />
660
- </>,
661
- () => Scheduler.log('Sync effect'),
662
- );
663
- await waitForAll(['a:a:one', 'b:b:one', 'Sync effect']);
664
-
665
- // Changes to part of the store (e.g. A) should not render other parts.
666
- source.valueA = 'a:two';
667
- await waitForAll(['a:a:two']);
668
- source.valueB = 'b:two';
669
- await waitForAll(['b:b:two']);
670
- });
671
- });
672
-
673
- // @gate enableUseMutableSource
674
- it('should detect tearing in part of the store not yet subscribed to', async () => {
675
- const source = createComplexSource('a:one', 'b:one');
676
- const mutableSource = createMutableSource(source, param => param.version);
677
-
678
- // Subscribe to part of the store.
679
- const getSnapshotA = s => s.valueA;
680
- const subscribeA = (s, callback) => s.subscribeA(callback);
681
- const getSnapshotB = s => s.valueB;
682
- const subscribeB = (s, callback) => s.subscribeB(callback);
683
-
684
- await act(async () => {
685
- ReactNoop.render(
686
- <>
687
- <Component
688
- label="a"
689
- getSnapshot={getSnapshotA}
690
- mutableSource={mutableSource}
691
- subscribe={subscribeA}
692
- />
693
- </>,
694
- () => Scheduler.log('Sync effect'),
695
- );
696
- await waitForAll(['a:a:one', 'Sync effect']);
697
-
698
- // Because the store has not changed yet, there are no pending updates,
699
- // so it is considered safe to read from when we start this render.
700
- React.startTransition(() => {
701
- ReactNoop.render(
702
- <>
703
- <Component
704
- label="a"
705
- getSnapshot={getSnapshotA}
706
- mutableSource={mutableSource}
707
- subscribe={subscribeA}
708
- />
709
- <Component
710
- label="b"
711
- getSnapshot={getSnapshotB}
712
- mutableSource={mutableSource}
713
- subscribe={subscribeB}
714
- />
715
- <Component
716
- label="c"
717
- getSnapshot={getSnapshotB}
718
- mutableSource={mutableSource}
719
- subscribe={subscribeB}
720
- />
721
- </>,
722
- () => Scheduler.log('Sync effect'),
723
- );
724
- });
725
-
726
- await waitFor(['a:a:one', 'b:b:one']);
727
-
728
- // Mutating the source should trigger a tear detection on the next read,
729
- // which should throw and re-render the entire tree.
730
- source.valueB = 'b:two';
731
-
732
- await waitForAll(['a:a:one', 'b:b:two', 'c:b:two', 'Sync effect']);
733
- });
734
- });
735
-
736
- // @gate enableUseMutableSource
737
- it('does not schedule an update for subscriptions that fire with an unchanged snapshot', async () => {
738
- const MockComponent = jest.fn(Component);
739
-
740
- const source = createSource('one');
741
- const mutableSource = createMutableSource(source, param => param.version);
742
-
743
- await act(async () => {
744
- ReactNoop.render(
745
- <MockComponent
746
- label="only"
747
- getSnapshot={defaultGetSnapshot}
748
- mutableSource={mutableSource}
749
- subscribe={defaultSubscribe}
750
- />,
751
- () => Scheduler.log('Sync effect'),
752
- );
753
- await waitFor(['only:one', 'Sync effect']);
754
- ReactNoop.flushPassiveEffects();
755
- expect(source.listenerCount).toBe(1);
756
-
757
- // Notify subscribe function but don't change the value
758
- source.value = 'one';
759
- await waitForAll([]);
760
- });
761
- });
762
-
763
- // @gate enableUseMutableSource
764
- it('should throw and restart if getSnapshot changes between scheduled update and re-render', async () => {
765
- const source = createSource('one');
766
- const mutableSource = createMutableSource(source, param => param.version);
767
-
768
- const newGetSnapshot = s => 'new:' + defaultGetSnapshot(s);
769
-
770
- let updateGetSnapshot;
771
-
772
- function WrapperWithState() {
773
- const tuple = React.useState(() => defaultGetSnapshot);
774
- updateGetSnapshot = tuple[1];
775
- return (
776
- <Component
777
- label="only"
778
- getSnapshot={tuple[0]}
779
- mutableSource={mutableSource}
780
- subscribe={defaultSubscribe}
781
- />
782
- );
783
- }
784
-
785
- await act(async () => {
786
- ReactNoop.render(<WrapperWithState />, () =>
787
- Scheduler.log('Sync effect'),
788
- );
789
- await waitForAll(['only:one', 'Sync effect']);
790
- ReactNoop.flushPassiveEffects();
791
-
792
- // Change the source (and schedule an update).
793
- source.value = 'two';
794
-
795
- // Schedule a higher priority update that changes getSnapshot.
796
- ReactNoop.flushSync(() => {
797
- updateGetSnapshot(() => newGetSnapshot);
798
- });
799
-
800
- assertLog(['only:new:two']);
801
- });
802
- });
803
-
804
- // @gate enableUseMutableSource
805
- it('should recover from a mutation during yield when other work is scheduled', async () => {
806
- const source = createSource('one');
807
- const mutableSource = createMutableSource(source, param => param.version);
808
-
809
- await act(async () => {
810
- // Start a render that uses the mutable source.
811
- React.startTransition(() => {
812
- ReactNoop.render(
813
- <>
814
- <Component
815
- label="a"
816
- getSnapshot={defaultGetSnapshot}
817
- mutableSource={mutableSource}
818
- subscribe={defaultSubscribe}
819
- />
820
- <Component
821
- label="b"
822
- getSnapshot={defaultGetSnapshot}
823
- mutableSource={mutableSource}
824
- subscribe={defaultSubscribe}
825
- />
826
- </>,
827
- );
828
- });
829
-
830
- await waitFor(['a:one']);
831
-
832
- // Mutate source
833
- source.value = 'two';
834
-
835
- // Now render something different.
836
- ReactNoop.render(<div />);
837
- await waitForAll([]);
838
- });
839
- });
840
-
841
- // @gate enableUseMutableSource
842
- it('should not throw if the new getSnapshot returns the same snapshot value', async () => {
843
- const source = createSource('one');
844
- const mutableSource = createMutableSource(source, param => param.version);
845
-
846
- const onRenderA = jest.fn();
847
- const onRenderB = jest.fn();
848
-
849
- let updateGetSnapshot;
850
-
851
- function WrapperWithState() {
852
- const tuple = React.useState(() => defaultGetSnapshot);
853
- updateGetSnapshot = tuple[1];
854
- return (
855
- <Component
856
- label="b"
857
- getSnapshot={tuple[0]}
858
- mutableSource={mutableSource}
859
- subscribe={defaultSubscribe}
860
- />
861
- );
862
- }
863
-
864
- await act(async () => {
865
- ReactNoop.render(
866
- <>
867
- <React.Profiler id="a" onRender={onRenderA}>
868
- <Component
869
- label="a"
870
- getSnapshot={defaultGetSnapshot}
871
- mutableSource={mutableSource}
872
- subscribe={defaultSubscribe}
873
- />
874
- </React.Profiler>
875
- <React.Profiler id="b" onRender={onRenderB}>
876
- <WrapperWithState />
877
- </React.Profiler>
878
- </>,
879
- () => Scheduler.log('Sync effect'),
880
- );
881
- await waitForAll(['a:one', 'b:one', 'Sync effect']);
882
- ReactNoop.flushPassiveEffects();
883
- expect(onRenderA).toHaveBeenCalledTimes(1);
884
- expect(onRenderB).toHaveBeenCalledTimes(1);
885
-
886
- // If B's getSnapshot function updates, but the snapshot it returns is the same,
887
- // only B should re-render (to update its state).
888
- updateGetSnapshot(() => s => defaultGetSnapshot(s));
889
- await waitForAll(['b:one']);
890
- ReactNoop.flushPassiveEffects();
891
- expect(onRenderA).toHaveBeenCalledTimes(1);
892
- expect(onRenderB).toHaveBeenCalledTimes(2);
893
- });
894
- });
895
-
896
- // @gate enableUseMutableSource
897
- it('should not throw if getSnapshot changes but the source can be safely read from anyway', async () => {
898
- const source = createSource('one');
899
- const mutableSource = createMutableSource(source, param => param.version);
900
-
901
- const newGetSnapshot = s => 'new:' + defaultGetSnapshot(s);
902
-
903
- let updateGetSnapshot;
904
-
905
- function WrapperWithState() {
906
- const tuple = React.useState(() => defaultGetSnapshot);
907
- updateGetSnapshot = tuple[1];
908
- return (
909
- <Component
910
- label="only"
911
- getSnapshot={tuple[0]}
912
- mutableSource={mutableSource}
913
- subscribe={defaultSubscribe}
914
- />
915
- );
916
- }
917
-
918
- await act(async () => {
919
- ReactNoop.render(<WrapperWithState />, () =>
920
- Scheduler.log('Sync effect'),
921
- );
922
- await waitForAll(['only:one', 'Sync effect']);
923
- ReactNoop.flushPassiveEffects();
924
-
925
- // Change the source (and schedule an update)
926
- // but also change the snapshot function too.
927
- ReactNoop.batchedUpdates(() => {
928
- source.value = 'two';
929
- updateGetSnapshot(() => newGetSnapshot);
930
- });
931
-
932
- await waitForAll(['only:new:two']);
933
- });
934
- });
935
-
936
- // @gate enableUseMutableSource
937
- it('should still schedule an update if an eager selector throws after a mutation', async () => {
938
- const source = createSource({
939
- friends: [
940
- {id: 1, name: 'Foo'},
941
- {id: 2, name: 'Bar'},
942
- ],
943
- });
944
- const mutableSource = createMutableSource(source, param => param.version);
945
-
946
- function FriendsList() {
947
- const getSnapshot = React.useCallback(
948
- ({value}) => Array.from(value.friends),
949
- [],
950
- );
951
- const friends = useMutableSource(
952
- mutableSource,
953
- getSnapshot,
954
- defaultSubscribe,
955
- );
956
- return (
957
- <ul>
958
- {friends.map(friend => (
959
- <Friend key={friend.id} id={friend.id} />
960
- ))}
961
- </ul>
962
- );
963
- }
964
-
965
- function Friend({id}) {
966
- const getSnapshot = React.useCallback(
967
- ({value}) => {
968
- // This selector is intentionally written in a way that will throw
969
- // if no matching friend exists in the store.
970
- return value.friends.find(friend => friend.id === id).name;
971
- },
972
- [id],
973
- );
974
- const name = useMutableSource(
975
- mutableSource,
976
- getSnapshot,
977
- defaultSubscribe,
978
- );
979
- Scheduler.log(`${id}:${name}`);
980
- return <li>{name}</li>;
981
- }
982
-
983
- await act(async () => {
984
- ReactNoop.render(<FriendsList />, () => Scheduler.log('Sync effect'));
985
- await waitForAll(['1:Foo', '2:Bar', 'Sync effect']);
986
-
987
- // This mutation will cause the "Bar" component to throw,
988
- // since its value will no longer be a part of the store.
989
- // Mutable source should still schedule an update though,
990
- // which should unmount "Bar" and mount "Baz".
991
- source.value = {
992
- friends: [
993
- {id: 1, name: 'Foo'},
994
- {id: 3, name: 'Baz'},
995
- ],
996
- };
997
- await waitForAll(['1:Foo', '3:Baz']);
998
- });
999
- });
1000
-
1001
- // @gate enableUseMutableSource
1002
- it('should not warn about updates that fire between unmount and passive unsubscribe', async () => {
1003
- const source = createSource('one');
1004
- const mutableSource = createMutableSource(source, param => param.version);
1005
-
1006
- function Wrapper() {
1007
- React.useLayoutEffect(() => () => {
1008
- Scheduler.log('layout unmount');
1009
- });
1010
- return (
1011
- <Component
1012
- label="only"
1013
- getSnapshot={defaultGetSnapshot}
1014
- mutableSource={mutableSource}
1015
- subscribe={defaultSubscribe}
1016
- />
1017
- );
1018
- }
1019
-
1020
- await act(async () => {
1021
- ReactNoop.renderToRootWithID(<Wrapper />, 'root', () =>
1022
- Scheduler.log('Sync effect'),
1023
- );
1024
- await waitForAll(['only:one', 'Sync effect']);
1025
- ReactNoop.flushPassiveEffects();
1026
-
1027
- // Unmounting a root should remove the remaining event listeners in a passive effect
1028
- ReactNoop.unmountRootWithID('root');
1029
- await waitFor(['layout unmount']);
1030
-
1031
- // Changes to source should not cause a warning,
1032
- // even though the unsubscribe hasn't run yet (since it's a pending passive effect).
1033
- source.value = 'two';
1034
- await waitForAll([]);
1035
- });
1036
- });
1037
-
1038
- // @gate enableUseMutableSource
1039
- it('should support inline selectors and updates that are processed after selector change', async () => {
1040
- const source = createSource({
1041
- a: 'initial',
1042
- b: 'initial',
1043
- });
1044
- const mutableSource = createMutableSource(source, param => param.version);
1045
-
1046
- const getSnapshotA = () => source.value.a;
1047
- const getSnapshotB = () => source.value.b;
1048
-
1049
- function mutateB(newB) {
1050
- source.value = {
1051
- ...source.value,
1052
- b: newB,
1053
- };
1054
- }
1055
-
1056
- function App({getSnapshot}) {
1057
- const state = useMutableSource(
1058
- mutableSource,
1059
- getSnapshot,
1060
- defaultSubscribe,
1061
- );
1062
- return state;
1063
- }
1064
-
1065
- const root = ReactNoop.createRoot();
1066
- await act(() => {
1067
- root.render(<App getSnapshot={getSnapshotA} />);
1068
- });
1069
- expect(root).toMatchRenderedOutput('initial');
1070
-
1071
- await act(() => {
1072
- mutateB('Updated B');
1073
- root.render(<App getSnapshot={getSnapshotB} />);
1074
- });
1075
- expect(root).toMatchRenderedOutput('Updated B');
1076
-
1077
- await act(() => {
1078
- mutateB('Another update');
1079
- });
1080
- expect(root).toMatchRenderedOutput('Another update');
1081
- });
1082
-
1083
- // @gate enableUseMutableSource
1084
- it('should clear the update queue when getSnapshot changes with pending lower priority updates', async () => {
1085
- const source = createSource({
1086
- a: 'initial',
1087
- b: 'initial',
1088
- });
1089
- const mutableSource = createMutableSource(source, param => param.version);
1090
-
1091
- const getSnapshotA = () => source.value.a;
1092
- const getSnapshotB = () => source.value.b;
1093
-
1094
- function mutateA(newA) {
1095
- source.value = {
1096
- ...source.value,
1097
- a: newA,
1098
- };
1099
- }
1100
-
1101
- function mutateB(newB) {
1102
- source.value = {
1103
- ...source.value,
1104
- b: newB,
1105
- };
1106
- }
1107
-
1108
- function App({toggle}) {
1109
- const state = useMutableSource(
1110
- mutableSource,
1111
- toggle ? getSnapshotB : getSnapshotA,
1112
- defaultSubscribe,
1113
- );
1114
- const result = (toggle ? 'B: ' : 'A: ') + state;
1115
- return result;
1116
- }
1117
-
1118
- const root = ReactNoop.createRoot();
1119
- await act(() => {
1120
- root.render(<App toggle={false} />);
1121
- });
1122
- expect(root).toMatchRenderedOutput('A: initial');
1123
-
1124
- await act(() => {
1125
- ReactNoop.discreteUpdates(() => {
1126
- // Update both A and B to the same value
1127
- mutateA('Update');
1128
- mutateB('Update');
1129
- // Toggle to B in the same batch
1130
- root.render(<App toggle={true} />);
1131
- });
1132
- // Mutate A at lower priority. This should never be rendered, because
1133
- // by the time we get to the lower priority, we've already switched
1134
- // to B.
1135
- mutateA('OOPS! This mutation should be ignored');
1136
- });
1137
- expect(root).toMatchRenderedOutput('B: Update');
1138
- });
1139
-
1140
- // @gate enableUseMutableSource
1141
- it('should clear the update queue when source changes with pending lower priority updates', async () => {
1142
- const sourceA = createSource('initial');
1143
- const sourceB = createSource('initial');
1144
- const mutableSourceA = createMutableSource(
1145
- sourceA,
1146
- param => param.versionA,
1147
- );
1148
- const mutableSourceB = createMutableSource(
1149
- sourceB,
1150
- param => param.versionB,
1151
- );
1152
-
1153
- function App({toggle}) {
1154
- const state = useMutableSource(
1155
- toggle ? mutableSourceB : mutableSourceA,
1156
- defaultGetSnapshot,
1157
- defaultSubscribe,
1158
- );
1159
- const result = (toggle ? 'B: ' : 'A: ') + state;
1160
- return result;
1161
- }
1162
-
1163
- const root = ReactNoop.createRoot();
1164
- await act(() => {
1165
- root.render(<App toggle={false} />);
1166
- });
1167
- expect(root).toMatchRenderedOutput('A: initial');
1168
-
1169
- await act(() => {
1170
- ReactNoop.discreteUpdates(() => {
1171
- // Update both A and B to the same value
1172
- sourceA.value = 'Update';
1173
- sourceB.value = 'Update';
1174
- // Toggle to B in the same batch
1175
- root.render(<App toggle={true} />);
1176
- });
1177
- // Mutate A at lower priority. This should never be rendered, because
1178
- // by the time we get to the lower priority, we've already switched
1179
- // to B.
1180
- sourceA.value = 'OOPS! This mutation should be ignored';
1181
- });
1182
- expect(root).toMatchRenderedOutput('B: Update');
1183
- });
1184
-
1185
- // @gate enableUseMutableSource
1186
- it('should always treat reading as potentially unsafe when getSnapshot changes between renders', async () => {
1187
- const source = createSource({
1188
- a: 'foo',
1189
- b: 'bar',
1190
- });
1191
- const mutableSource = createMutableSource(source, param => param.version);
1192
-
1193
- const getSnapshotA = () => source.value.a;
1194
- const getSnapshotB = () => source.value.b;
1195
-
1196
- function mutateA(newA) {
1197
- source.value = {
1198
- ...source.value,
1199
- a: newA,
1200
- };
1201
- }
1202
-
1203
- function App({getSnapshotFirst, getSnapshotSecond}) {
1204
- const first = useMutableSource(
1205
- mutableSource,
1206
- getSnapshotFirst,
1207
- defaultSubscribe,
1208
- );
1209
- const second = useMutableSource(
1210
- mutableSource,
1211
- getSnapshotSecond,
1212
- defaultSubscribe,
1213
- );
1214
-
1215
- let result = `x: ${first}, y: ${second}`;
1216
-
1217
- if (getSnapshotFirst === getSnapshotSecond) {
1218
- // When both getSnapshot functions are equal,
1219
- // the two values must be consistent.
1220
- if (first !== second) {
1221
- result = 'Oops, tearing!';
1222
- }
1223
- }
1224
-
1225
- React.useEffect(() => {
1226
- Scheduler.log(result);
1227
- }, [result]);
1228
-
1229
- return result;
1230
- }
1231
-
1232
- const root = ReactNoop.createRoot();
1233
- await act(() => {
1234
- root.render(
1235
- <App
1236
- getSnapshotFirst={getSnapshotA}
1237
- getSnapshotSecond={getSnapshotB}
1238
- />,
1239
- );
1240
- });
1241
- // x and y start out reading from different parts of the store.
1242
- assertLog(['x: foo, y: bar']);
1243
-
1244
- await act(() => {
1245
- ReactNoop.discreteUpdates(() => {
1246
- // At high priority, toggle y so that it reads from A instead of B.
1247
- // Simultaneously, mutate A.
1248
- mutateA('baz');
1249
- root.render(
1250
- <App
1251
- getSnapshotFirst={getSnapshotA}
1252
- getSnapshotSecond={getSnapshotA}
1253
- />,
1254
- );
1255
-
1256
- // If this update were processed before the next mutation,
1257
- // it would be expected to yield "baz" and "baz".
1258
- });
1259
-
1260
- // At lower priority, mutate A again.
1261
- // This happens to match the initial value of B.
1262
- mutateA('bar');
1263
-
1264
- // When this update is processed,
1265
- // it is expected to yield "bar" and "bar".
1266
- });
1267
-
1268
- // Check that we didn't commit any inconsistent states.
1269
- // The actual sequence of work will be:
1270
- // 1. React renders the high-pri update, sees a new getSnapshot, detects the source has been further mutated, and throws
1271
- // 2. React re-renders with all pending updates, including the second mutation, and renders "bar" and "bar".
1272
- assertLog(['x: bar, y: bar']);
1273
- });
1274
-
1275
- // @gate enableUseMutableSource
1276
- it('getSnapshot changes and then source is mutated in between paint and passive effect phase', async () => {
1277
- const source = createSource({
1278
- a: 'foo',
1279
- b: 'bar',
1280
- });
1281
- const mutableSource = createMutableSource(source, param => param.version);
1282
-
1283
- function mutateB(newB) {
1284
- source.value = {
1285
- ...source.value,
1286
- b: newB,
1287
- };
1288
- }
1289
-
1290
- const getSnapshotA = () => source.value.a;
1291
- const getSnapshotB = () => source.value.b;
1292
-
1293
- function App({getSnapshot}) {
1294
- const value = useMutableSource(
1295
- mutableSource,
1296
- getSnapshot,
1297
- defaultSubscribe,
1298
- );
1299
-
1300
- Scheduler.log('Render: ' + value);
1301
- React.useEffect(() => {
1302
- Scheduler.log('Commit: ' + value);
1303
- }, [value]);
1304
-
1305
- return value;
1306
- }
1307
-
1308
- const root = ReactNoop.createRoot();
1309
- await act(() => {
1310
- root.render(<App getSnapshot={getSnapshotA} />);
1311
- });
1312
- assertLog(['Render: foo', 'Commit: foo']);
1313
-
1314
- await act(async () => {
1315
- // Switch getSnapshot to read from B instead
1316
- root.render(<App getSnapshot={getSnapshotB} />);
1317
- // Render and finish the tree, but yield right after paint, before
1318
- // the passive effects have fired.
1319
- await waitForPaint(['Render: bar']);
1320
- // Then mutate B.
1321
- mutateB('baz');
1322
- });
1323
- assertLog([
1324
- // Fires the effect from the previous render
1325
- 'Commit: bar',
1326
- // During that effect, it should detect that the snapshot has changed
1327
- // and re-render.
1328
- 'Render: baz',
1329
- 'Commit: baz',
1330
- ]);
1331
- expect(root).toMatchRenderedOutput('baz');
1332
- });
1333
-
1334
- // @gate enableUseMutableSource
1335
- it('getSnapshot changes and then source is mutated in between paint and passive effect phase, case 2', async () => {
1336
- const source = createSource({
1337
- a: 'a0',
1338
- b: 'b0',
1339
- });
1340
- const mutableSource = createMutableSource(source, param => param.version);
1341
-
1342
- const getSnapshotA = () => source.value.a;
1343
- const getSnapshotB = () => source.value.b;
1344
-
1345
- function mutateA(newA) {
1346
- source.value = {
1347
- ...source.value,
1348
- a: newA,
1349
- };
1350
- }
1351
-
1352
- function App({getSnapshotFirst, getSnapshotSecond}) {
1353
- const first = useMutableSource(
1354
- mutableSource,
1355
- getSnapshotFirst,
1356
- defaultSubscribe,
1357
- );
1358
- const second = useMutableSource(
1359
- mutableSource,
1360
- getSnapshotSecond,
1361
- defaultSubscribe,
1362
- );
1363
-
1364
- return `first: ${first}, second: ${second}`;
1365
- }
1366
-
1367
- const root = ReactNoop.createRoot();
1368
- await act(() => {
1369
- root.render(
1370
- <App
1371
- getSnapshotFirst={getSnapshotA}
1372
- getSnapshotSecond={getSnapshotB}
1373
- />,
1374
- );
1375
- });
1376
- expect(root.getChildrenAsJSX()).toEqual('first: a0, second: b0');
1377
-
1378
- await act(async () => {
1379
- // Switch the second getSnapshot to also read from A
1380
- root.render(
1381
- <App
1382
- getSnapshotFirst={getSnapshotA}
1383
- getSnapshotSecond={getSnapshotA}
1384
- />,
1385
- );
1386
- // Render and finish the tree, but yield right after paint, before
1387
- // the passive effects have fired.
1388
- await waitForPaint([]);
1389
-
1390
- // Now mutate A. Both hooks should update.
1391
- // This is at high priority so that it doesn't get batched with default
1392
- // priority updates that might fire during the passive effect
1393
- await act(() => {
1394
- ReactNoop.discreteUpdates(() => {
1395
- mutateA('a1');
1396
- });
1397
- });
1398
-
1399
- expect(root).toMatchRenderedOutput('first: a1, second: a1');
1400
- });
1401
-
1402
- expect(root.getChildrenAsJSX()).toEqual('first: a1, second: a1');
1403
- });
1404
-
1405
- // @gate enableUseMutableSource
1406
- it(
1407
- 'if source is mutated after initial read but before subscription is set ' +
1408
- 'up, should still entangle all pending mutations even if snapshot of ' +
1409
- 'new subscription happens to match',
1410
- async () => {
1411
- const source = createSource({
1412
- a: 'a0',
1413
- b: 'b0',
1414
- });
1415
- const mutableSource = createMutableSource(source, param => param.version);
1416
-
1417
- const getSnapshotA = () => source.value.a;
1418
- const getSnapshotB = () => source.value.b;
1419
-
1420
- function mutateA(newA) {
1421
- source.value = {
1422
- ...source.value,
1423
- a: newA,
1424
- };
1425
- }
1426
-
1427
- function mutateB(newB) {
1428
- source.value = {
1429
- ...source.value,
1430
- b: newB,
1431
- };
1432
- }
1433
-
1434
- function Read({getSnapshot}) {
1435
- const value = useMutableSource(
1436
- mutableSource,
1437
- getSnapshot,
1438
- defaultSubscribe,
1439
- );
1440
- Scheduler.log(value);
1441
- return value;
1442
- }
1443
-
1444
- function Text({text}) {
1445
- Scheduler.log(text);
1446
- return text;
1447
- }
1448
-
1449
- const root = ReactNoop.createRoot();
1450
- await act(() => {
1451
- root.render(
1452
- <>
1453
- <Read getSnapshot={getSnapshotA} />
1454
- </>,
1455
- );
1456
- });
1457
- assertLog(['a0']);
1458
- expect(root).toMatchRenderedOutput('a0');
1459
-
1460
- await act(async () => {
1461
- React.startTransition(() => {
1462
- root.render(
1463
- <>
1464
- <Read getSnapshot={getSnapshotA} />
1465
- <Read getSnapshot={getSnapshotB} />
1466
- <Text text="c" />
1467
- </>,
1468
- );
1469
- });
1470
-
1471
- await waitFor(['a0', 'b0']);
1472
- // Mutate in an event. This schedules a subscription update on a, which
1473
- // already mounted, but not b, which hasn't subscribed yet.
1474
- if (gate(flags => flags.enableUnifiedSyncLane)) {
1475
- React.startTransition(() => {
1476
- mutateA('a1');
1477
- mutateB('b1');
1478
- });
1479
- } else {
1480
- mutateA('a1');
1481
- mutateB('b1');
1482
- }
1483
-
1484
- // Mutate again at lower priority. This will schedule another subscription
1485
- // update on a, but not b. When b mounts and subscriptions, the value it
1486
- // read during render will happen to match the latest value. But it should
1487
- // still entangle the updates to prevent the previous update (a1) from
1488
- // rendering by itself.
1489
- React.startTransition(() => {
1490
- mutateA('a0');
1491
- mutateB('b0');
1492
- });
1493
- // Finish the current render
1494
- await waitForPaint(['c']);
1495
- // a0 will re-render because of the mutation update. But it should show
1496
- // the latest value, not the intermediate one, to avoid tearing with b.
1497
- await waitForPaint(['a0']);
1498
-
1499
- expect(root).toMatchRenderedOutput('a0b0c');
1500
- // We should be done.
1501
- await waitForAll([]);
1502
- expect(root).toMatchRenderedOutput('a0b0c');
1503
- });
1504
- },
1505
- );
1506
-
1507
- // @gate enableUseMutableSource
1508
- it('warns about functions being used as snapshot values', async () => {
1509
- const source = createSource(() => 'a');
1510
- const mutableSource = createMutableSource(source, param => param.version);
1511
-
1512
- const getSnapshot = () => source.value;
1513
-
1514
- function Read() {
1515
- const fn = useMutableSource(mutableSource, getSnapshot, defaultSubscribe);
1516
- const value = fn();
1517
- Scheduler.log(value);
1518
- return value;
1519
- }
1520
-
1521
- const root = ReactNoop.createRoot();
1522
- root.render(
1523
- <>
1524
- <Read />
1525
- </>,
1526
- );
1527
- await expect(async () => await waitForAll(['a'])).toErrorDev(
1528
- 'Mutable source should not return a function as the snapshot value.',
1529
- );
1530
- expect(root).toMatchRenderedOutput('a');
1531
- });
1532
-
1533
- // @gate enableUseMutableSource
1534
- it('getSnapshot changes and then source is mutated during interleaved event', async () => {
1535
- const {useEffect} = React;
1536
-
1537
- const source = createComplexSource('1', '2');
1538
- const mutableSource = createMutableSource(source, param => param.version);
1539
-
1540
- // Subscribe to part of the store.
1541
- const getSnapshotA = s => s.valueA;
1542
- const subscribeA = (s, callback) => s.subscribeA(callback);
1543
- const configA = [getSnapshotA, subscribeA];
1544
-
1545
- const getSnapshotB = s => s.valueB;
1546
- const subscribeB = (s, callback) => s.subscribeB(callback);
1547
- const configB = [getSnapshotB, subscribeB];
1548
-
1549
- function App({parentConfig, childConfig}) {
1550
- const [getSnapshot, subscribe] = parentConfig;
1551
- const parentValue = useMutableSource(
1552
- mutableSource,
1553
- getSnapshot,
1554
- subscribe,
1555
- );
1556
-
1557
- Scheduler.log('Parent: ' + parentValue);
1558
-
1559
- return (
1560
- <Child
1561
- parentConfig={parentConfig}
1562
- childConfig={childConfig}
1563
- parentValue={parentValue}
1564
- />
1565
- );
1566
- }
1567
-
1568
- function Child({parentConfig, childConfig, parentValue}) {
1569
- const [getSnapshot, subscribe] = childConfig;
1570
- const childValue = useMutableSource(
1571
- mutableSource,
1572
- getSnapshot,
1573
- subscribe,
1574
- );
1575
-
1576
- Scheduler.log('Child: ' + childValue);
1577
-
1578
- let result = `${parentValue}, ${childValue}`;
1579
-
1580
- if (parentConfig === childConfig) {
1581
- // When both components read using the same config, the two values
1582
- // must be consistent.
1583
- if (parentValue !== childValue) {
1584
- result = 'Oops, tearing!';
1585
- }
1586
- }
1587
-
1588
- useEffect(() => {
1589
- Scheduler.log('Commit: ' + result);
1590
- }, [result]);
1591
-
1592
- return result;
1593
- }
1594
-
1595
- const root = ReactNoop.createRoot();
1596
- await act(() => {
1597
- root.render(<App parentConfig={configA} childConfig={configB} />);
1598
- });
1599
- assertLog(['Parent: 1', 'Child: 2', 'Commit: 1, 2']);
1600
-
1601
- await act(async () => {
1602
- // Switch the parent and the child to read using the same config
1603
- React.startTransition(() => {
1604
- root.render(<App parentConfig={configB} childConfig={configB} />);
1605
- });
1606
- // Start rendering the parent, but yield before rendering the child
1607
- await waitFor(['Parent: 2']);
1608
-
1609
- // Mutate the config. This is at lower priority so that 1) to make sure
1610
- // it doesn't happen to get batched with the in-progress render, and 2)
1611
- // so it doesn't interrupt the in-progress render.
1612
- React.startTransition(() => {
1613
- source.valueB = '3';
1614
- });
1615
-
1616
- if (gate(flags => flags.forceConcurrentByDefaultForTesting)) {
1617
- await waitFor([
1618
- // The partial render completes
1619
- 'Child: 2',
1620
- 'Commit: 2, 2',
1621
- ]);
1622
-
1623
- // Now there are two pending mutations at different priorities. But they
1624
- // both read the same version of the mutable source, so we must render
1625
- // them simultaneously.
1626
- //
1627
- await waitFor([
1628
- 'Parent: 3',
1629
- // Demonstrates that we can yield here
1630
- ]);
1631
- await waitFor([
1632
- // Now finish the rest of the update
1633
- 'Child: 3',
1634
- 'Commit: 3, 3',
1635
- ]);
1636
- } else {
1637
- // In default sync mode, all of the updates flush sync.
1638
- await waitFor([
1639
- // The partial render completes
1640
- 'Child: 2',
1641
- 'Commit: 2, 2',
1642
- 'Parent: 3',
1643
- 'Child: 3',
1644
- ]);
1645
-
1646
- await waitForAll([
1647
- // Now finish the rest of the update
1648
- 'Commit: 3, 3',
1649
- ]);
1650
- }
1651
- });
1652
- });
1653
-
1654
- // @gate enableUseMutableSource
1655
- it('should not tear with newly mounted component when updates were scheduled at a lower priority', async () => {
1656
- const source = createSource('one');
1657
- const mutableSource = createMutableSource(source, param => param.version);
1658
-
1659
- let committedA = null;
1660
- let committedB = null;
1661
-
1662
- const onRender = () => {
1663
- if (committedB !== null) {
1664
- expect(committedA).toBe(committedB);
1665
- }
1666
- };
1667
-
1668
- function ComponentA() {
1669
- const snapshot = useMutableSource(
1670
- mutableSource,
1671
- defaultGetSnapshot,
1672
- defaultSubscribe,
1673
- );
1674
- Scheduler.log(`a:${snapshot}`);
1675
- React.useEffect(() => {
1676
- committedA = snapshot;
1677
- }, [snapshot]);
1678
- return <div>{`a:${snapshot}`}</div>;
1679
- }
1680
- function ComponentB() {
1681
- const snapshot = useMutableSource(
1682
- mutableSource,
1683
- defaultGetSnapshot,
1684
- defaultSubscribe,
1685
- );
1686
- Scheduler.log(`b:${snapshot}`);
1687
- React.useEffect(() => {
1688
- committedB = snapshot;
1689
- }, [snapshot]);
1690
- return <div>{`b:${snapshot}`}</div>;
1691
- }
1692
-
1693
- // Mount ComponentA with data version 1
1694
- await act(() => {
1695
- ReactNoop.render(
1696
- <React.Profiler id="root" onRender={onRender}>
1697
- <ComponentA />
1698
- </React.Profiler>,
1699
- () => Scheduler.log('Sync effect'),
1700
- );
1701
- });
1702
- assertLog(['a:one', 'Sync effect']);
1703
- expect(source.listenerCount).toBe(1);
1704
-
1705
- // Mount ComponentB with version 1 (but don't commit it)
1706
- await act(async () => {
1707
- ReactNoop.render(
1708
- <React.Profiler id="root" onRender={onRender}>
1709
- <ComponentA />
1710
- <ComponentB />
1711
- </React.Profiler>,
1712
- () => Scheduler.log('Sync effect'),
1713
- );
1714
- await waitFor(['a:one', 'b:one', 'Sync effect']);
1715
- expect(source.listenerCount).toBe(1);
1716
-
1717
- // Mutate -> schedule update for ComponentA
1718
- React.startTransition(() => {
1719
- source.value = 'two';
1720
- });
1721
-
1722
- // Commit ComponentB -> notice the change and schedule an update for ComponentB
1723
- await waitForAll(['a:two', 'b:two']);
1724
- expect(source.listenerCount).toBe(2);
1725
- });
1726
- });
1727
-
1728
- if (__DEV__) {
1729
- describe('dev warnings', () => {
1730
- // @gate enableUseMutableSource
1731
- it('should warn if the subscribe function does not return an unsubscribe function', async () => {
1732
- const source = createSource('one');
1733
- const mutableSource = createMutableSource(
1734
- source,
1735
- param => param.version,
1736
- );
1737
-
1738
- const brokenSubscribe = () => {};
1739
-
1740
- await expect(async () => {
1741
- await act(() => {
1742
- ReactNoop.render(
1743
- <Component
1744
- label="only"
1745
- getSnapshot={defaultGetSnapshot}
1746
- mutableSource={mutableSource}
1747
- subscribe={brokenSubscribe}
1748
- />,
1749
- );
1750
- });
1751
- }).toErrorDev(
1752
- 'Mutable source subscribe function must return an unsubscribe function.',
1753
- );
1754
- });
1755
-
1756
- // @gate enableUseMutableSource
1757
- it('should error if multiple renderers of the same type use a mutable source at the same time', async () => {
1758
- const source = createSource('one');
1759
- const mutableSource = createMutableSource(
1760
- source,
1761
- param => param.version,
1762
- );
1763
-
1764
- await act(async () => {
1765
- // Start a render that uses the mutable source.
1766
- React.startTransition(() => {
1767
- ReactNoop.render(
1768
- <>
1769
- <Component
1770
- label="a"
1771
- getSnapshot={defaultGetSnapshot}
1772
- mutableSource={mutableSource}
1773
- subscribe={defaultSubscribe}
1774
- />
1775
- <Component
1776
- label="b"
1777
- getSnapshot={defaultGetSnapshot}
1778
- mutableSource={mutableSource}
1779
- subscribe={defaultSubscribe}
1780
- />
1781
- </>,
1782
- );
1783
- });
1784
-
1785
- await waitFor(['a:one']);
1786
-
1787
- const PrevScheduler = Scheduler;
1788
-
1789
- // Get a new copy of ReactNoop.
1790
- loadModules();
1791
-
1792
- spyOnDev(console, 'error').mockImplementation(() => {});
1793
-
1794
- // Use the mutablesource again but with a different renderer.
1795
- ReactNoop.render(
1796
- <Component
1797
- label="c"
1798
- getSnapshot={defaultGetSnapshot}
1799
- mutableSource={mutableSource}
1800
- subscribe={defaultSubscribe}
1801
- />,
1802
- );
1803
- await waitFor(['c:one']);
1804
-
1805
- expect(console.error.mock.calls[0][0]).toContain(
1806
- 'Detected multiple renderers concurrently rendering the ' +
1807
- 'same mutable source. This is currently unsupported.',
1808
- );
1809
-
1810
- // TODO (useMutableSource) Act will automatically flush remaining work from render 1,
1811
- // but at this point something in the hooks dispatcher has been broken by jest.resetModules()
1812
- // Figure out what this is and remove this catch.
1813
- expect(() =>
1814
- PrevScheduler.unstable_flushAllWithoutAsserting(),
1815
- ).toThrow('Invalid hook call');
1816
- });
1817
- });
1818
-
1819
- // @gate enableUseMutableSource
1820
- it('should error if multiple renderers of the same type use a mutable source at the same time with mutation between', async () => {
1821
- const source = createSource('one');
1822
- const mutableSource = createMutableSource(
1823
- source,
1824
- param => param.version,
1825
- );
1826
-
1827
- await act(async () => {
1828
- // Start a render that uses the mutable source.
1829
- React.startTransition(() => {
1830
- ReactNoop.render(
1831
- <>
1832
- <Component
1833
- label="a"
1834
- getSnapshot={defaultGetSnapshot}
1835
- mutableSource={mutableSource}
1836
- subscribe={defaultSubscribe}
1837
- />
1838
- <Component
1839
- label="b"
1840
- getSnapshot={defaultGetSnapshot}
1841
- mutableSource={mutableSource}
1842
- subscribe={defaultSubscribe}
1843
- />
1844
- </>,
1845
- );
1846
- });
1847
-
1848
- await waitFor(['a:one']);
1849
-
1850
- const PrevScheduler = Scheduler;
1851
-
1852
- // Get a new copy of ReactNoop.
1853
- loadModules();
1854
-
1855
- spyOnDev(console, 'error').mockImplementation(() => {});
1856
-
1857
- // Mutate before the new render reads from the source.
1858
- source.value = 'two';
1859
-
1860
- // Use the mutablesource again but with a different renderer.
1861
- ReactNoop.render(
1862
- <Component
1863
- label="c"
1864
- getSnapshot={defaultGetSnapshot}
1865
- mutableSource={mutableSource}
1866
- subscribe={defaultSubscribe}
1867
- />,
1868
- );
1869
- await waitFor(['c:two']);
1870
-
1871
- expect(console.error.mock.calls[0][0]).toContain(
1872
- 'Detected multiple renderers concurrently rendering the ' +
1873
- 'same mutable source. This is currently unsupported.',
1874
- );
1875
-
1876
- // TODO (useMutableSource) Act will automatically flush remaining work from render 1,
1877
- // but at this point something in the hooks dispatcher has been broken by jest.resetModules()
1878
- // Figure out what this is and remove this catch.
1879
- expect(() =>
1880
- PrevScheduler.unstable_flushAllWithoutAsserting(),
1881
- ).toThrow('Invalid hook call');
1882
- });
1883
- });
1884
- });
1885
- }
1886
-});