[DevTools] Send Suspense rects to frontend (#34170)
Sebastian "Sebbie" Silbermann committed
Aug 12, 2025 at 16:48 UTC
de06211dbe0ce641a435b52ed3c868720aa9c633
12 files changed
+586
-134
packages/react-devtools-shared/src/__tests__/profilingCommitTreeBuilder-test.js
+6
@@ -228,6 +228,8 @@ describe('commit tree', () => {
228
[root]
229
▾ <App>
230
<Suspense>
231
+ [shell]
232
+ <Suspense name="App>?" rects={null}>
233
`);
234
utils.act(() => modernRender(<App renderChildren={true} />));
235
expect(store).toMatchInlineSnapshot(`
@@ -235,6 +237,8 @@ describe('commit tree', () => {
237
▾ <App>
238
▾ <Suspense>
239
<LazyInnerComponent>
240
+ [shell]
241
+ <Suspense name="App>?" rects={null}>
242
`);
243
utils.act(() => modernRender(<App renderChildren={false} />));
244
expect(store).toMatchInlineSnapshot(`
@@ -299,6 +303,8 @@ describe('commit tree', () => {
303
[root]
304
▾ <App>
305
<Suspense>
306
+ [shell]
307
+ <Suspense name="App>?" rects={null}>
308
`);
309
utils.act(() => modernRender(<App renderChildren={false} />));
310
expect(store).toMatchInlineSnapshot(`
packages/react-devtools-shared/src/__tests__/store-test.js
+173
-58
@@ -24,6 +24,16 @@ describe('Store', () => {
24
let store;
25
let withErrorsOrWarningsIgnored;
26
27
+ beforeAll(() => {
28
+ // JSDDOM doesn't implement getClientRects so we're just faking one for testing purposes
29
+ Element.prototype.getClientRects = function (this: Element) {
30
+ const textContent = this.textContent;
31
+ return [
32
+ new DOMRect(1, 2, textContent.length, textContent.split('\n').length),
33
+ ];
34
+ };
35
+ });
36
+
37
beforeEach(() => {
38
global.IS_REACT_ACT_ENVIRONMENT = true;
39
@@ -123,6 +133,8 @@ describe('Store', () => {
133
<Suspense>
134
▾ <Parent>
135
<Child>
136
+ [shell]
137
+ <Suspense name="Unknown" rects={null}>
138
`);
139
});
140
@@ -480,6 +492,8 @@ describe('Store', () => {
492
<Component key="Outside">
493
▾ <Suspense>
494
<Loading>
495
+ [shell]
496
+ <Suspense name="Wrapper>?" rects={null}>
497
`);
498
499
await act(() => {
@@ -491,6 +505,8 @@ describe('Store', () => {
505
<Component key="Outside">
506
▾ <Suspense>
507
<Component key="Inside">
508
+ [shell]
509
+ <Suspense name="Wrapper>?" rects={[{x:1,y:2,width:5,height:1}]}>
510
`);
511
});
512
@@ -513,23 +529,31 @@ describe('Store', () => {
529
}) => (
530
<React.Fragment>
531
<Component key="Outside" />
516
- <React.Suspense fallback={<Loading key="Parent Fallback" />}>
532
+ <React.Suspense
533
+ name="parent"
534
+ fallback={<Loading key="Parent Fallback" />}>
535
<Component key="Unrelated at Start" />
518
- <React.Suspense fallback={<Loading key="Suspense 1 Fallback" />}>
536
+ <React.Suspense
537
+ name="one"
538
+ fallback={<Loading key="Suspense 1 Fallback" />}>
539
{suspendFirst ? (
540
<Never />
541
) : (
542
<Component key="Suspense 1 Content" />
543
)}
544
</React.Suspense>
525
- <React.Suspense fallback={<Loading key="Suspense 2 Fallback" />}>
545
+ <React.Suspense
546
+ name="two"
547
+ fallback={<Loading key="Suspense 2 Fallback" />}>
548
{suspendSecond ? (
549
<Never />
550
) : (
551
<Component key="Suspense 2 Content" />
552
)}
553
</React.Suspense>
532
- <React.Suspense fallback={<Loading key="Suspense 3 Fallback" />}>
554
+ <React.Suspense
555
+ name="three"
556
+ fallback={<Loading key="Suspense 3 Fallback" />}>
557
<Never />
558
</React.Suspense>
559
{suspendParent && <Never />}
@@ -538,7 +562,7 @@ describe('Store', () => {
562
</React.Fragment>
563
);
564
541
- await act(() =>
565
+ await actAsync(() =>
566
render(
567
<Wrapper
568
suspendParent={false}
@@ -551,15 +575,20 @@ describe('Store', () => {
575
[root]
576
▾ <Wrapper>
577
<Component key="Outside">
554
- ▾ <Suspense>
578
+ ▾ <Suspense name="parent">
579
<Component key="Unrelated at Start">
556
- ▾ <Suspense>
580
+ ▾ <Suspense name="one">
581
<Component key="Suspense 1 Content">
558
- ▾ <Suspense>
582
+ ▾ <Suspense name="two">
583
<Component key="Suspense 2 Content">
560
- ▾ <Suspense>
584
+ ▾ <Suspense name="three">
585
<Loading key="Suspense 3 Fallback">
586
<Component key="Unrelated at End">
587
+ [shell]
588
+ <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}]}>
589
+ <Suspense name="one" rects={null}>
590
+ <Suspense name="two" rects={null}>
591
+ <Suspense name="three" rects={null}>
592
`);
593
await act(() =>
594
render(
@@ -574,15 +603,20 @@ describe('Store', () => {
603
[root]
604
▾ <Wrapper>
605
<Component key="Outside">
577
- ▾ <Suspense>
606
+ ▾ <Suspense name="parent">
607
<Component key="Unrelated at Start">
579
- ▾ <Suspense>
608
+ ▾ <Suspense name="one">
609
<Loading key="Suspense 1 Fallback">
581
- ▾ <Suspense>
610
+ ▾ <Suspense name="two">
611
<Component key="Suspense 2 Content">
583
- ▾ <Suspense>
612
+ ▾ <Suspense name="three">
613
<Loading key="Suspense 3 Fallback">
614
<Component key="Unrelated at End">
615
+ [shell]
616
+ <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
617
+ <Suspense name="one" rects={null}>
618
+ <Suspense name="two" rects={null}>
619
+ <Suspense name="three" rects={null}>
620
`);
621
await act(() =>
622
render(
@@ -597,15 +631,20 @@ describe('Store', () => {
631
[root]
632
▾ <Wrapper>
633
<Component key="Outside">
600
- ▾ <Suspense>
634
+ ▾ <Suspense name="parent">
635
<Component key="Unrelated at Start">
602
- ▾ <Suspense>
636
+ ▾ <Suspense name="one">
637
<Component key="Suspense 1 Content">
604
- ▾ <Suspense>
638
+ ▾ <Suspense name="two">
639
<Loading key="Suspense 2 Fallback">
606
- ▾ <Suspense>
640
+ ▾ <Suspense name="three">
641
<Loading key="Suspense 3 Fallback">
642
<Component key="Unrelated at End">
643
+ [shell]
644
+ <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
645
+ <Suspense name="one" rects={null}>
646
+ <Suspense name="two" rects={null}>
647
+ <Suspense name="three" rects={null}>
648
`);
649
await act(() =>
650
render(
@@ -620,15 +659,20 @@ describe('Store', () => {
659
[root]
660
▾ <Wrapper>
661
<Component key="Outside">
623
- ▾ <Suspense>
662
+ ▾ <Suspense name="parent">
663
<Component key="Unrelated at Start">
625
- ▾ <Suspense>
664
+ ▾ <Suspense name="one">
665
<Loading key="Suspense 1 Fallback">
627
- ▾ <Suspense>
666
+ ▾ <Suspense name="two">
667
<Component key="Suspense 2 Content">
629
- ▾ <Suspense>
668
+ ▾ <Suspense name="three">
669
<Loading key="Suspense 3 Fallback">
670
<Component key="Unrelated at End">
671
+ [shell]
672
+ <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
673
+ <Suspense name="one" rects={null}>
674
+ <Suspense name="two" rects={null}>
675
+ <Suspense name="three" rects={null}>
676
`);
677
await act(() =>
678
render(
@@ -643,8 +687,13 @@ describe('Store', () => {
687
[root]
688
▾ <Wrapper>
689
<Component key="Outside">
646
- ▾ <Suspense>
690
+ ▾ <Suspense name="parent">
691
<Loading key="Parent Fallback">
692
+ [shell]
693
+ <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
694
+ <Suspense name="one" rects={null}>
695
+ <Suspense name="two" rects={null}>
696
+ <Suspense name="three" rects={null}>
697
`);
698
await act(() =>
699
render(
@@ -659,15 +708,20 @@ describe('Store', () => {
708
[root]
709
▾ <Wrapper>
710
<Component key="Outside">
662
- ▾ <Suspense>
711
+ ▾ <Suspense name="parent">
712
<Component key="Unrelated at Start">
664
- ▾ <Suspense>
713
+ ▾ <Suspense name="one">
714
<Loading key="Suspense 1 Fallback">
666
- ▾ <Suspense>
715
+ ▾ <Suspense name="two">
716
<Loading key="Suspense 2 Fallback">
668
- ▾ <Suspense>
717
+ ▾ <Suspense name="three">
718
<Loading key="Suspense 3 Fallback">
719
<Component key="Unrelated at End">
720
+ [shell]
721
+ <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
722
+ <Suspense name="one" rects={null}>
723
+ <Suspense name="two" rects={null}>
724
+ <Suspense name="three" rects={null}>
725
`);
726
await act(() =>
727
render(
@@ -682,15 +736,20 @@ describe('Store', () => {
736
[root]
737
▾ <Wrapper>
738
<Component key="Outside">
685
- ▾ <Suspense>
739
+ ▾ <Suspense name="parent">
740
<Component key="Unrelated at Start">
687
- ▾ <Suspense>
741
+ ▾ <Suspense name="one">
742
<Component key="Suspense 1 Content">
689
- ▾ <Suspense>
743
+ ▾ <Suspense name="two">
744
<Component key="Suspense 2 Content">
691
- ▾ <Suspense>
745
+ ▾ <Suspense name="three">
746
<Loading key="Suspense 3 Fallback">
747
<Component key="Unrelated at End">
748
+ [shell]
749
+ <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}]}>
750
+ <Suspense name="one" rects={null}>
751
+ <Suspense name="two" rects={null}>
752
+ <Suspense name="three" rects={null}>
753
`);
754
755
const rendererID = getRendererID();
@@ -705,15 +764,20 @@ describe('Store', () => {
764
[root]
765
▾ <Wrapper>
766
<Component key="Outside">
708
- ▾ <Suspense>
767
+ ▾ <Suspense name="parent">
768
<Component key="Unrelated at Start">
710
- ▾ <Suspense>
769
+ ▾ <Suspense name="one">
770
<Loading key="Suspense 1 Fallback">
712
- ▾ <Suspense>
771
+ ▾ <Suspense name="two">
772
<Component key="Suspense 2 Content">
714
- ▾ <Suspense>
773
+ ▾ <Suspense name="three">
774
<Loading key="Suspense 3 Fallback">
775
<Component key="Unrelated at End">
776
+ [shell]
777
+ <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
778
+ <Suspense name="one" rects={null}>
779
+ <Suspense name="two" rects={null}>
780
+ <Suspense name="three" rects={null}>
781
`);
782
await act(() =>
783
agent.overrideSuspense({
@@ -726,8 +790,13 @@ describe('Store', () => {
790
[root]
791
▾ <Wrapper>
792
<Component key="Outside">
729
- ▾ <Suspense>
793
+ ▾ <Suspense name="parent">
794
<Loading key="Parent Fallback">
795
+ [shell]
796
+ <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
797
+ <Suspense name="one" rects={null}>
798
+ <Suspense name="two" rects={null}>
799
+ <Suspense name="three" rects={null}>
800
`);
801
await act(() =>
802
render(
@@ -742,8 +811,13 @@ describe('Store', () => {
811
[root]
812
▾ <Wrapper>
813
<Component key="Outside">
745
- ▾ <Suspense>
814
+ ▾ <Suspense name="parent">
815
<Loading key="Parent Fallback">
816
+ [shell]
817
+ <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
818
+ <Suspense name="one" rects={null}>
819
+ <Suspense name="two" rects={null}>
820
+ <Suspense name="three" rects={null}>
821
`);
822
await act(() =>
823
agent.overrideSuspense({
@@ -756,15 +830,20 @@ describe('Store', () => {
830
[root]
831
▾ <Wrapper>
832
<Component key="Outside">
759
- ▾ <Suspense>
833
+ ▾ <Suspense name="parent">
834
<Component key="Unrelated at Start">
761
- ▾ <Suspense>
835
+ ▾ <Suspense name="one">
836
<Loading key="Suspense 1 Fallback">
763
- ▾ <Suspense>
837
+ ▾ <Suspense name="two">
838
<Loading key="Suspense 2 Fallback">
765
- ▾ <Suspense>
839
+ ▾ <Suspense name="three">
840
<Loading key="Suspense 3 Fallback">
841
<Component key="Unrelated at End">
842
+ [shell]
843
+ <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
844
+ <Suspense name="one" rects={null}>
845
+ <Suspense name="two" rects={null}>
846
+ <Suspense name="three" rects={null}>
847
`);
848
await act(() =>
849
agent.overrideSuspense({
@@ -777,15 +856,20 @@ describe('Store', () => {
856
[root]
857
▾ <Wrapper>
858
<Component key="Outside">
780
- ▾ <Suspense>
859
+ ▾ <Suspense name="parent">
860
<Component key="Unrelated at Start">
782
- ▾ <Suspense>
861
+ ▾ <Suspense name="one">
862
<Loading key="Suspense 1 Fallback">
784
- ▾ <Suspense>
863
+ ▾ <Suspense name="two">
864
<Loading key="Suspense 2 Fallback">
786
- ▾ <Suspense>
865
+ ▾ <Suspense name="three">
866
<Loading key="Suspense 3 Fallback">
867
<Component key="Unrelated at End">
868
+ [shell]
869
+ <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
870
+ <Suspense name="one" rects={null}>
871
+ <Suspense name="two" rects={null}>
872
+ <Suspense name="three" rects={null}>
873
`);
874
await act(() =>
875
render(
@@ -800,15 +884,20 @@ describe('Store', () => {
884
[root]
885
▾ <Wrapper>
886
<Component key="Outside">
803
- ▾ <Suspense>
887
+ ▾ <Suspense name="parent">
888
<Component key="Unrelated at Start">
805
- ▾ <Suspense>
889
+ ▾ <Suspense name="one">
890
<Component key="Suspense 1 Content">
807
- ▾ <Suspense>
891
+ ▾ <Suspense name="two">
892
<Component key="Suspense 2 Content">
809
- ▾ <Suspense>
893
+ ▾ <Suspense name="three">
894
<Loading key="Suspense 3 Fallback">
895
<Component key="Unrelated at End">
896
+ [shell]
897
+ <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}]}>
898
+ <Suspense name="one" rects={null}>
899
+ <Suspense name="two" rects={null}>
900
+ <Suspense name="three" rects={null}>
901
`);
902
});
903
@@ -848,6 +937,8 @@ describe('Store', () => {
937
<Component key="A">
938
▾ <Suspense>
939
<Loading>
940
+ [shell]
941
+ <Suspense name="Wrapper>?" rects={null}>
942
`);
943
944
await act(() => {
@@ -861,6 +952,8 @@ describe('Store', () => {
952
▾ <Suspense>
953
<Component key="B">
954
<Component key="C">
955
+ [shell]
956
+ <Suspense name="Wrapper>?" rects={[{x:1,y:2,width:5,height:1}]}>
957
`);
958
});
959
@@ -1197,6 +1290,8 @@ describe('Store', () => {
1290
expect(store).toMatchInlineSnapshot(`
1291
[root]
1292
▸ <Wrapper>
1293
+ [shell]
1294
+ <Suspense name="Wrapper>?" rects={null}>
1295
`);
1296
1297
// This test isn't meaningful unless we expand the suspended tree
@@ -1212,6 +1307,8 @@ describe('Store', () => {
1307
<Component key="Outside">
1308
▾ <Suspense>
1309
<Loading>
1310
+ [shell]
1311
+ <Suspense name="Wrapper>?" rects={null}>
1312
`);
1313
1314
await act(() => {
@@ -1223,6 +1320,8 @@ describe('Store', () => {
1320
<Component key="Outside">
1321
▾ <Suspense>
1322
<Component key="Inside">
1323
+ [shell]
1324
+ <Suspense name="Wrapper>?" rects={[{x:1,y:2,width:5,height:1}]}>
1325
`);
1326
});
1327
@@ -1447,6 +1546,8 @@ describe('Store', () => {
1546
expect(store).toMatchInlineSnapshot(`
1547
[root]
1548
▸ <SuspenseTree>
1549
+ [shell]
1550
+ <Suspense name="SuspenseTree>?" rects={null}>
1551
`);
1552
1553
await act(() =>
@@ -1460,6 +1561,8 @@ describe('Store', () => {
1561
▾ <SuspenseTree>
1562
▾ <Suspense>
1563
▸ <Parent>
1564
+ [shell]
1565
+ <Suspense name="SuspenseTree>?" rects={null}>
1566
`);
1567
1568
const rendererID = getRendererID();
@@ -1477,6 +1580,8 @@ describe('Store', () => {
1580
▾ <SuspenseTree>
1581
▾ <Suspense>
1582
<Fallback>
1583
+ [shell]
1584
+ <Suspense name="SuspenseTree>?" rects={null}>
1585
`);
1586
1587
await act(() =>
@@ -1491,6 +1596,8 @@ describe('Store', () => {
1596
▾ <SuspenseTree>
1597
▾ <Suspense>
1598
▸ <Parent>
1599
+ [shell]
1600
+ <Suspense name="SuspenseTree>?" rects={null}>
1601
`);
1602
});
1603
});
@@ -1794,6 +1901,8 @@ describe('Store', () => {
1901
[root]
1902
▾ <App>
1903
<Suspense>
1904
+ [shell]
1905
+ <Suspense name="App>?" rects={null}>
1906
`);
1907
1908
await Promise.resolve();
@@ -1806,6 +1915,8 @@ describe('Store', () => {
1915
▾ <App>
1916
▾ <Suspense>
1917
<LazyInnerComponent>
1918
+ [shell]
1919
+ <Suspense name="App>?" rects={null}>
1920
`);
1921
1922
// Render again to unmount it
@@ -2291,20 +2402,24 @@ describe('Store', () => {
2402
await actAsync(() => render(<App renderA={true} />));
2403
2404
expect(store).toMatchInlineSnapshot(`
2294
- [root]
2295
- ▾ <App>
2296
- ▾ <Suspense>
2297
- <ChildA>
2298
- `);
2405
+ [root]
2406
+ ▾ <App>
2407
+ ▾ <Suspense>
2408
+ <ChildA>
2409
+ [shell]
2410
+ <Suspense name="App>?" rects={null}>
2411
+ `);
2412
2413
await actAsync(() => render(<App renderA={false} />));
2414
2415
expect(store).toMatchInlineSnapshot(`
2303
- [root]
2304
- ▾ <App>
2305
- ▾ <Suspense>
2306
- <ChildB>
2307
- `);
2416
+ [root]
2417
+ ▾ <App>
2418
+ ▾ <Suspense>
2419
+ <ChildB>
2420
+ [shell]
2421
+ <Suspense name="App>?" rects={null}>
2422
+ `);
2423
});
2424
});
2425
packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js
+9
@@ -156,6 +156,9 @@ describe('Store component filters', () => {
156
<div>
157
▾ <Suspense>
158
<div>
159
+ [shell]
160
+ <Suspense name="Unknown" rects={[]}>
161
+ <Suspense name="Unknown" rects={[]}>
162
`);
163
164
await actAsync(
@@ -171,6 +174,9 @@ describe('Store component filters', () => {
174
<div>
175
▾ <Suspense>
176
<div>
177
+ [shell]
178
+ <Suspense name="Unknown" rects={[]}>
179
+ <Suspense name="Unknown" rects={[]}>
180
`);
181
182
await actAsync(
@@ -186,6 +192,9 @@ describe('Store component filters', () => {
192
<div>
193
▾ <Suspense>
194
<div>
195
+ [shell]
196
+ <Suspense name="Unknown" rects={[]}>
197
+ <Suspense name="Unknown" rects={[]}>
198
`);
199
});
200
packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js
+47
-51
@@ -32,7 +32,7 @@ describe('StoreStressConcurrent', () => {
32
// this helper with the real thing.
33
actAsync = require('./utils').actAsync;
34
35
- print = require('./__serializers__/storeSerializer').print;
35
+ print = require('./__serializers__/storeSerializer').printStore;
36
});
37
38
// This is a stress test for the tree mount/update/unmount traversal.
@@ -67,8 +67,7 @@ describe('StoreStressConcurrent', () => {
67
let container = document.createElement('div');
68
let root = ReactDOMClient.createRoot(container);
69
act(() => root.render(<Parent>{[a, b, c, d, e]}</Parent>));
70
- expect(store).toMatchInlineSnapshot(
71
- `
70
+ expect(store).toMatchInlineSnapshot(`
71
[root]
72
▾ <Parent>
73
<A key="a">
@@ -76,8 +75,7 @@ describe('StoreStressConcurrent', () => {
75
<C key="c">
76
<D key="d">
77
<E key="e">
79
- `,
80
- );
78
+ `);
79
expect(container.textContent).toMatch('abcde');
80
const snapshotForABCDE = print(store);
81
@@ -86,8 +84,7 @@ describe('StoreStressConcurrent', () => {
84
act(() => {
85
setShowX(true);
86
});
89
- expect(store).toMatchInlineSnapshot(
90
- `
87
+ expect(store).toMatchInlineSnapshot(`
88
[root]
89
▾ <Parent>
90
<A key="a">
@@ -96,8 +93,7 @@ describe('StoreStressConcurrent', () => {
93
<X>
94
<D key="d">
95
<E key="e">
99
- `,
100
- );
96
+ `);
97
expect(container.textContent).toMatch('abxde');
98
const snapshotForABXDE = print(store);
99
@@ -419,7 +415,7 @@ describe('StoreStressConcurrent', () => {
415
),
416
);
417
// We snapshot each step once so it doesn't regress.d
422
- snapshots.push(print(store));
418
+ snapshots.push(print(store, false, null, false));
419
await act(() => root.unmount());
420
expect(print(store)).toBe('');
421
}
@@ -524,7 +520,7 @@ describe('StoreStressConcurrent', () => {
520
</Root>,
521
),
522
);
527
- expect(print(store)).toEqual(snapshots[i]);
523
+ expect(print(store, false, null, false)).toEqual(snapshots[i]);
524
await act(() => root.unmount());
525
expect(print(store)).toBe('');
526
}
@@ -544,7 +540,7 @@ describe('StoreStressConcurrent', () => {
540
</Root>,
541
),
542
);
547
- expect(print(store)).toEqual(snapshots[i]);
543
+ expect(print(store, false, null, false)).toEqual(snapshots[i]);
544
// Re-render with steps[j].
545
await act(() =>
546
root.render(
@@ -556,7 +552,7 @@ describe('StoreStressConcurrent', () => {
552
),
553
);
554
// Verify the successful transition to steps[j].
559
- expect(print(store)).toEqual(snapshots[j]);
555
+ expect(print(store, false, null, false)).toEqual(snapshots[j]);
556
// Check that we can transition back again.
557
await act(() =>
558
root.render(
@@ -567,7 +563,7 @@ describe('StoreStressConcurrent', () => {
563
</Root>,
564
),
565
);
570
- expect(print(store)).toEqual(snapshots[i]);
566
+ expect(print(store, false, null, false)).toEqual(snapshots[i]);
567
// Clean up after every iteration.
568
await act(() => root.unmount());
569
expect(print(store)).toBe('');
@@ -593,7 +589,7 @@ describe('StoreStressConcurrent', () => {
589
</Root>,
590
),
591
);
596
- expect(print(store)).toEqual(snapshots[i]);
592
+ expect(print(store, false, null, false)).toEqual(snapshots[i]);
593
// Re-render with steps[j].
594
await act(() =>
595
root.render(
@@ -609,7 +605,7 @@ describe('StoreStressConcurrent', () => {
605
),
606
);
607
// Verify the successful transition to steps[j].
612
- expect(print(store)).toEqual(snapshots[j]);
608
+ expect(print(store, false, null, false)).toEqual(snapshots[j]);
609
// Check that we can transition back again.
610
await act(() =>
611
root.render(
@@ -624,7 +620,7 @@ describe('StoreStressConcurrent', () => {
620
</Root>,
621
),
622
);
627
- expect(print(store)).toEqual(snapshots[i]);
623
+ expect(print(store, false, null, false)).toEqual(snapshots[i]);
624
// Clean up after every iteration.
625
await act(() => root.unmount());
626
expect(print(store)).toBe('');
@@ -646,7 +642,7 @@ describe('StoreStressConcurrent', () => {
642
</Root>,
643
),
644
);
649
- expect(print(store)).toEqual(snapshots[i]);
645
+ expect(print(store, false, null, false)).toEqual(snapshots[i]);
646
// Re-render with steps[j].
647
await act(() =>
648
root.render(
@@ -662,7 +658,7 @@ describe('StoreStressConcurrent', () => {
658
),
659
);
660
// Verify the successful transition to steps[j].
665
- expect(print(store)).toEqual(snapshots[j]);
661
+ expect(print(store, false, null, false)).toEqual(snapshots[j]);
662
// Check that we can transition back again.
663
await act(() =>
664
root.render(
@@ -673,7 +669,7 @@ describe('StoreStressConcurrent', () => {
669
</Root>,
670
),
671
);
676
- expect(print(store)).toEqual(snapshots[i]);
672
+ expect(print(store, false, null, false)).toEqual(snapshots[i]);
673
// Clean up after every iteration.
674
await act(() => root.unmount());
675
expect(print(store)).toBe('');
@@ -699,7 +695,7 @@ describe('StoreStressConcurrent', () => {
695
</Root>,
696
),
697
);
702
- expect(print(store)).toEqual(snapshots[i]);
698
+ expect(print(store, false, null, false)).toEqual(snapshots[i]);
699
// Re-render with steps[j].
700
await act(() =>
701
root.render(
@@ -711,7 +707,7 @@ describe('StoreStressConcurrent', () => {
707
),
708
);
709
// Verify the successful transition to steps[j].
714
- expect(print(store)).toEqual(snapshots[j]);
710
+ expect(print(store, false, null, false)).toEqual(snapshots[j]);
711
// Check that we can transition back again.
712
await act(() =>
713
root.render(
@@ -726,7 +722,7 @@ describe('StoreStressConcurrent', () => {
722
</Root>,
723
),
724
);
729
- expect(print(store)).toEqual(snapshots[i]);
725
+ expect(print(store, false, null, false)).toEqual(snapshots[i]);
726
// Clean up after every iteration.
727
await act(() => root.unmount());
728
expect(print(store)).toBe('');
@@ -755,7 +751,7 @@ describe('StoreStressConcurrent', () => {
751
const suspenseID = store.getElementIDAtIndex(2);
752
753
// Force fallback.
758
- expect(print(store)).toEqual(snapshots[i]);
754
+ expect(print(store, false, null, false)).toEqual(snapshots[i]);
755
await actAsync(async () => {
756
bridge.send('overrideSuspense', {
757
id: suspenseID,
@@ -763,7 +759,7 @@ describe('StoreStressConcurrent', () => {
759
forceFallback: true,
760
});
761
});
766
- expect(print(store)).toEqual(snapshots[j]);
762
+ expect(print(store, false, null, false)).toEqual(snapshots[j]);
763
764
// Stop forcing fallback.
765
await actAsync(async () => {
@@ -773,7 +769,7 @@ describe('StoreStressConcurrent', () => {
769
forceFallback: false,
770
});
771
});
776
- expect(print(store)).toEqual(snapshots[i]);
772
+ expect(print(store, false, null, false)).toEqual(snapshots[i]);
773
774
// Trigger actual fallback.
775
await act(() =>
@@ -789,7 +785,7 @@ describe('StoreStressConcurrent', () => {
785
</Root>,
786
),
787
);
792
- expect(print(store)).toEqual(snapshots[j]);
788
+ expect(print(store, false, null, false)).toEqual(snapshots[j]);
789
790
// Force fallback while we're in fallback mode.
791
await act(() => {
@@ -800,7 +796,7 @@ describe('StoreStressConcurrent', () => {
796
});
797
});
798
// Keep seeing fallback content.
803
- expect(print(store)).toEqual(snapshots[j]);
799
+ expect(print(store, false, null, false)).toEqual(snapshots[j]);
800
801
// Switch to primary mode.
802
await act(() =>
@@ -813,7 +809,7 @@ describe('StoreStressConcurrent', () => {
809
),
810
);
811
// Fallback is still forced though.
816
- expect(print(store)).toEqual(snapshots[j]);
812
+ expect(print(store, false, null, false)).toEqual(snapshots[j]);
813
814
// Stop forcing fallback. This reverts to primary content.
815
await actAsync(async () => {
@@ -824,7 +820,7 @@ describe('StoreStressConcurrent', () => {
820
});
821
});
822
// Now we see primary content.
827
- expect(print(store)).toEqual(snapshots[i]);
823
+ expect(print(store, false, null, false)).toEqual(snapshots[i]);
824
825
// Clean up after every iteration.
826
await actAsync(async () => root.unmount());
@@ -910,7 +906,7 @@ describe('StoreStressConcurrent', () => {
906
),
907
);
908
// We snapshot each step once so it doesn't regress.
913
- snapshots.push(print(store));
909
+ snapshots.push(print(store, false, null, false));
910
await act(() => root.unmount());
911
expect(print(store)).toBe('');
912
}
@@ -935,7 +931,7 @@ describe('StoreStressConcurrent', () => {
931
),
932
);
933
// We snapshot each step once so it doesn't regress.
938
- fallbackSnapshots.push(print(store));
934
+ fallbackSnapshots.push(print(store, false, null, false));
935
await act(() => root.unmount());
936
expect(print(store)).toBe('');
937
}
@@ -1065,7 +1061,7 @@ describe('StoreStressConcurrent', () => {
1061
</Root>,
1062
),
1063
);
1068
- expect(print(store)).toEqual(snapshots[i]);
1064
+ expect(print(store, false, null, false)).toEqual(snapshots[i]);
1065
// Re-render with steps[j].
1066
await act(() =>
1067
root.render(
@@ -1079,7 +1075,7 @@ describe('StoreStressConcurrent', () => {
1075
),
1076
);
1077
// Verify the successful transition to steps[j].
1082
- expect(print(store)).toEqual(snapshots[j]);
1078
+ expect(print(store, false, null, false)).toEqual(snapshots[j]);
1079
// Check that we can transition back again.
1080
await act(() =>
1081
root.render(
@@ -1092,7 +1088,7 @@ describe('StoreStressConcurrent', () => {
1088
</Root>,
1089
),
1090
);
1095
- expect(print(store)).toEqual(snapshots[i]);
1091
+ expect(print(store, false, null, false)).toEqual(snapshots[i]);
1092
// Clean up after every iteration.
1093
await act(() => root.unmount());
1094
expect(print(store)).toBe('');
@@ -1121,7 +1117,7 @@ describe('StoreStressConcurrent', () => {
1117
</Root>,
1118
),
1119
);
1124
- expect(print(store)).toEqual(fallbackSnapshots[i]);
1120
+ expect(print(store, false, null, false)).toEqual(fallbackSnapshots[i]);
1121
// Re-render with steps[j].
1122
await act(() =>
1123
root.render(
@@ -1140,7 +1136,7 @@ describe('StoreStressConcurrent', () => {
1136
),
1137
);
1138
// Verify the successful transition to steps[j].
1143
- expect(print(store)).toEqual(fallbackSnapshots[j]);
1139
+ expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]);
1140
// Check that we can transition back again.
1141
await act(() =>
1142
root.render(
@@ -1158,7 +1154,7 @@ describe('StoreStressConcurrent', () => {
1154
</Root>,
1155
),
1156
);
1161
- expect(print(store)).toEqual(fallbackSnapshots[i]);
1157
+ expect(print(store, false, null, false)).toEqual(fallbackSnapshots[i]);
1158
// Clean up after every iteration.
1159
await act(() => root.unmount());
1160
expect(print(store)).toBe('');
@@ -1182,7 +1178,7 @@ describe('StoreStressConcurrent', () => {
1178
</Root>,
1179
),
1180
);
1185
- expect(print(store)).toEqual(snapshots[i]);
1181
+ expect(print(store, false, null, false)).toEqual(snapshots[i]);
1182
// Re-render with steps[j].
1183
await act(() =>
1184
root.render(
@@ -1196,7 +1192,7 @@ describe('StoreStressConcurrent', () => {
1192
),
1193
);
1194
// Verify the successful transition to steps[j].
1199
- expect(print(store)).toEqual(fallbackSnapshots[j]);
1195
+ expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]);
1196
// Check that we can transition back again.
1197
await act(() =>
1198
root.render(
@@ -1209,7 +1205,7 @@ describe('StoreStressConcurrent', () => {
1205
</Root>,
1206
),
1207
);
1212
- expect(print(store)).toEqual(snapshots[i]);
1208
+ expect(print(store, false, null, false)).toEqual(snapshots[i]);
1209
// Clean up after every iteration.
1210
await act(() => root.unmount());
1211
expect(print(store)).toBe('');
@@ -1233,7 +1229,7 @@ describe('StoreStressConcurrent', () => {
1229
</Root>,
1230
),
1231
);
1236
- expect(print(store)).toEqual(fallbackSnapshots[i]);
1232
+ expect(print(store, false, null, false)).toEqual(fallbackSnapshots[i]);
1233
// Re-render with steps[j].
1234
await act(() =>
1235
root.render(
@@ -1247,7 +1243,7 @@ describe('StoreStressConcurrent', () => {
1243
),
1244
);
1245
// Verify the successful transition to steps[j].
1250
- expect(print(store)).toEqual(snapshots[j]);
1246
+ expect(print(store, false, null, false)).toEqual(snapshots[j]);
1247
// Check that we can transition back again.
1248
await act(() =>
1249
root.render(
@@ -1260,7 +1256,7 @@ describe('StoreStressConcurrent', () => {
1256
</Root>,
1257
),
1258
);
1263
- expect(print(store)).toEqual(fallbackSnapshots[i]);
1259
+ expect(print(store, false, null, false)).toEqual(fallbackSnapshots[i]);
1260
// Clean up after every iteration.
1261
await act(() => root.unmount());
1262
expect(print(store)).toBe('');
@@ -1291,7 +1287,7 @@ describe('StoreStressConcurrent', () => {
1287
const suspenseID = store.getElementIDAtIndex(2);
1288
1289
// Force fallback.
1294
- expect(print(store)).toEqual(snapshots[i]);
1290
+ expect(print(store, false, null, false)).toEqual(snapshots[i]);
1291
await actAsync(async () => {
1292
bridge.send('overrideSuspense', {
1293
id: suspenseID,
@@ -1299,7 +1295,7 @@ describe('StoreStressConcurrent', () => {
1295
forceFallback: true,
1296
});
1297
});
1302
- expect(print(store)).toEqual(fallbackSnapshots[j]);
1298
+ expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]);
1299
1300
// Stop forcing fallback.
1301
await actAsync(async () => {
@@ -1309,7 +1305,7 @@ describe('StoreStressConcurrent', () => {
1305
forceFallback: false,
1306
});
1307
});
1312
- expect(print(store)).toEqual(snapshots[i]);
1308
+ expect(print(store, false, null, false)).toEqual(snapshots[i]);
1309
1310
// Trigger actual fallback.
1311
await act(() =>
@@ -1323,7 +1319,7 @@ describe('StoreStressConcurrent', () => {
1319
</Root>,
1320
),
1321
);
1326
- expect(print(store)).toEqual(fallbackSnapshots[j]);
1322
+ expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]);
1323
1324
// Force fallback while we're in fallback mode.
1325
await act(() => {
@@ -1334,7 +1330,7 @@ describe('StoreStressConcurrent', () => {
1330
});
1331
});
1332
// Keep seeing fallback content.
1337
- expect(print(store)).toEqual(fallbackSnapshots[j]);
1333
+ expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]);
1334
1335
// Switch to primary mode.
1336
await act(() =>
@@ -1349,7 +1345,7 @@ describe('StoreStressConcurrent', () => {
1345
),
1346
);
1347
// Fallback is still forced though.
1352
- expect(print(store)).toEqual(fallbackSnapshots[j]);
1348
+ expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]);
1349
1350
// Stop forcing fallback. This reverts to primary content.
1351
await actAsync(async () => {
@@ -1360,7 +1356,7 @@ describe('StoreStressConcurrent', () => {
1356
});
1357
});
1358
// Now we see primary content.
1363
- expect(print(store)).toEqual(snapshots[i]);
1359
+ expect(print(store, false, null, false)).toEqual(snapshots[i]);
1360
1361
// Clean up after every iteration.
1362
await act(() => root.unmount());
packages/react-devtools-shared/src/__tests__/treeContext-test.js
+31
-13
@@ -1368,6 +1368,9 @@ describe('TreeListContext', () => {
1368
▾ <Child>
1369
▾ <Suspense>
1370
<Grandchild>
1371
+ [shell]
1372
+ <Suspense name="Parent>?" rects={null}>
1373
+ <Suspense name="Child>?" rects={null}>
1374
`);
1375
1376
const outerSuspenseID = ((store.getElementIDAtIndex(1): any): number);
@@ -1407,6 +1410,9 @@ describe('TreeListContext', () => {
1410
▾ <Child>
1411
▾ <Suspense>
1412
<Grandchild>
1413
+ [shell]
1414
+ <Suspense name="Parent>?" rects={null}>
1415
+ <Suspense name="Child>?" rects={null}>
1416
`);
1417
});
1418
});
@@ -2361,16 +2367,20 @@ describe('TreeListContext', () => {
2367
jest.runAllTimers();
2368
2369
expect(state).toMatchInlineSnapshot(`
2364
- [root]
2365
- <Suspense>
2366
- `);
2370
+ [root]
2371
+ <Suspense>
2372
+ [shell]
2373
+ <Suspense name="Unknown" rects={null}>
2374
+ `);
2375
2376
selectNextErrorOrWarning();
2377
2378
expect(state).toMatchInlineSnapshot(`
2371
- [root]
2372
- <Suspense>
2373
- `);
2379
+ [root]
2380
+ <Suspense>
2381
+ [shell]
2382
+ <Suspense name="Unknown" rects={null}>
2383
+ `);
2384
});
2385
2386
it('should properly handle errors/warnings from components that dont mount because of Suspense', async () => {
@@ -2392,9 +2402,11 @@ describe('TreeListContext', () => {
2402
utils.act(() => TestRenderer.create(<Contexts />));
2403
2404
expect(state).toMatchInlineSnapshot(`
2395
- [root]
2396
- <Suspense>
2397
- `);
2405
+ [root]
2406
+ <Suspense>
2407
+ [shell]
2408
+ <Suspense name="Unknown" rects={null}>
2409
+ `);
2410
2411
await Promise.resolve();
2412
withErrorsOrWarningsIgnored(['test-only:'], () =>
@@ -2414,6 +2426,8 @@ describe('TreeListContext', () => {
2426
▾ <Suspense>
2427
<Child> ⚠
2428
<Child>
2429
+ [shell]
2430
+ <Suspense name="Unknown" rects={null}>
2431
`);
2432
});
2433
@@ -2442,6 +2456,8 @@ describe('TreeListContext', () => {
2456
▾ <Suspense>
2457
▾ <Fallback>
2458
<Child> ✕
2459
+ [shell]
2460
+ <Suspense name="Unknown" rects={null}>
2461
`);
2462
2463
await Promise.resolve();
@@ -2456,10 +2472,12 @@ describe('TreeListContext', () => {
2472
);
2473
2474
expect(state).toMatchInlineSnapshot(`
2459
- [root]
2460
- ▾ <Suspense>
2461
- <Child>
2462
- `);
2475
+ [root]
2476
+ ▾ <Suspense>
2477
+ <Child>
2478
+ [shell]
2479
+ <Suspense name="Unknown" rects={null}>
2480
+ `);
2481
});
2482
});
2483
packages/react-devtools-shared/src/backend/fiber/renderer.js
+77
-3
@@ -86,6 +86,7 @@ import {
86
SUSPENSE_TREE_OPERATION_ADD,
87
SUSPENSE_TREE_OPERATION_REMOVE,
88
SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
89
+ SUSPENSE_TREE_OPERATION_RESIZE,
90
} from '../../constants';
91
import {inspectHooksOfFiber} from 'react-debug-tools';
92
import {
@@ -2558,6 +2559,20 @@ export function attach(
2559
pushOperation(fiberID);
2560
pushOperation(parentID);
2561
pushOperation(nameStringID);
2562
+
2563
+ const rects = suspenseInstance.rects;
2564
+ if (rects === null) {
2565
+ pushOperation(-1);
2566
+ } else {
2567
+ pushOperation(rects.length);
2568
+ for (let i = 0; i < rects.length; ++i) {
2569
+ const rect = rects[i];
2570
+ pushOperation(Math.round(rect.x));
2571
+ pushOperation(Math.round(rect.y));
2572
+ pushOperation(Math.round(rect.width));
2573
+ pushOperation(Math.round(rect.height));
2574
+ }
2575
+ }
2576
}
2577
2578
function recordUnmount(fiberInstance: FiberInstance): void {
@@ -2606,7 +2621,30 @@ export function attach(
2621
}
2622
2623
function recordSuspenseResize(suspenseNode: SuspenseNode): void {
2609
- // TODO: Notify the front end of the change.
2624
+ if (__DEBUG__) {
2625
+ console.log('recordSuspenseResize()', suspenseNode);
2626
+ }
2627
+ const fiberInstance = suspenseNode.instance;
2628
+ if (fiberInstance.kind !== FIBER_INSTANCE) {
2629
+ // TODO: Resizes of filtered Suspense nodes are currently dropped.
2630
+ return;
2631
+ }
2632
+
2633
+ pushOperation(SUSPENSE_TREE_OPERATION_RESIZE);
2634
+ pushOperation(fiberInstance.id);
2635
+ const rects = suspenseNode.rects;
2636
+ if (rects === null) {
2637
+ pushOperation(-1);
2638
+ } else {
2639
+ pushOperation(rects.length);
2640
+ for (let i = 0; i < rects.length; ++i) {
2641
+ const rect = rects[i];
2642
+ pushOperation(Math.round(rect.x));
2643
+ pushOperation(Math.round(rect.y));
2644
+ pushOperation(Math.round(rect.width));
2645
+ pushOperation(Math.round(rect.height));
2646
+ }
2647
+ }
2648
}
2649
2650
function recordSuspenseUnmount(suspenseInstance: SuspenseNode): void {
@@ -3442,7 +3480,25 @@ export function attach(
3480
// Measure this Suspense node. In general we shouldn't do this until we have
3481
// inserted the new children but since we know this is a FiberInstance we'll
3482
// just use the Fiber anyway.
3445
- newSuspenseNode.rects = measureInstance(newInstance);
3483
+ // Fallbacks get attributed to the parent so we only measure if we're
3484
+ // showing primary content.
3485
+ if (OffscreenComponent === -1) {
3486
+ const isTimedOut = fiber.memoizedState !== null;
3487
+ if (!isTimedOut) {
3488
+ newSuspenseNode.rects = measureInstance(newInstance);
3489
+ }
3490
+ } else {
3491
+ const contentFiber = fiber.child;
3492
+ if (contentFiber === null) {
3493
+ throw new Error(
3494
+ 'There should always be an Offscreen Fiber child in a Suspense boundary.',
3495
+ );
3496
+ }
3497
+ const isTimedOut = fiber.memoizedState !== null;
3498
+ if (!isTimedOut) {
3499
+ newSuspenseNode.rects = measureInstance(newInstance);
3500
+ }
3501
+ }
3502
recordSuspenseMount(newSuspenseNode, reconcilingParentSuspenseNode);
3503
}
3504
insertChild(newInstance);
@@ -3476,7 +3532,25 @@ export function attach(
3532
// Measure this Suspense node. In general we shouldn't do this until we have
3533
// inserted the new children but since we know this is a FiberInstance we'll
3534
// just use the Fiber anyway.
3479
- newSuspenseNode.rects = measureInstance(newInstance);
3535
+ // Fallbacks get attributed to the parent so we only measure if we're
3536
+ // showing primary content.
3537
+ if (OffscreenComponent === -1) {
3538
+ const isTimedOut = fiber.memoizedState !== null;
3539
+ if (!isTimedOut) {
3540
+ newSuspenseNode.rects = measureInstance(newInstance);
3541
+ }
3542
+ } else {
3543
+ const contentFiber = fiber.child;
3544
+ if (contentFiber === null) {
3545
+ throw new Error(
3546
+ 'There should always be an Offscreen Fiber child in a Suspense boundary.',
3547
+ );
3548
+ }
3549
+ const isTimedOut = fiber.memoizedState !== null;
3550
+ if (!isTimedOut) {
3551
+ newSuspenseNode.rects = measureInstance(newInstance);
3552
+ }
3553
+ }
3554
}
3555
insertChild(newInstance);
3556
if (__DEBUG__) {
packages/react-devtools-shared/src/constants.js
+1
@@ -27,6 +27,7 @@ export const TREE_OPERATION_SET_SUBTREE_MODE = 7;
27
export const SUSPENSE_TREE_OPERATION_ADD = 8;
28
export const SUSPENSE_TREE_OPERATION_REMOVE = 9;
29
export const SUSPENSE_TREE_OPERATION_REORDER_CHILDREN = 10;
30
+export const SUSPENSE_TREE_OPERATION_RESIZE = 11;
31
32
export const PROFILING_FLAG_BASIC_SUPPORT = 0b01;
33
export const PROFILING_FLAG_TIMELINE_SUPPORT = 0b10;
packages/react-devtools-shared/src/devtools/store.js
+74
-2
@@ -23,6 +23,7 @@ import {
23
SUSPENSE_TREE_OPERATION_ADD,
24
SUSPENSE_TREE_OPERATION_REMOVE,
25
SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
26
+ SUSPENSE_TREE_OPERATION_RESIZE,
27
} from '../constants';
28
import {ElementTypeRoot} from '../frontend/types';
29
import {
@@ -1418,6 +1419,7 @@ export default class Store extends EventEmitter<{
1419
const id = operations[i + 1];
1420
const parentID = operations[i + 2];
1421
const nameStringID = operations[i + 3];
1422
+ const numRects = ((operations[i + 4]: any): number);
1423
let name = stringTable[nameStringID];
1424
1425
if (this._idToSuspense.has(id)) {
@@ -1448,6 +1450,22 @@ export default class Store extends EventEmitter<{
1450
}
1451
}
1452
1453
+ i += 5;
1454
+ let rects: SuspenseNode['rects'];
1455
+ if (numRects === -1) {
1456
+ rects = null;
1457
+ } else {
1458
+ rects = [];
1459
+ for (let rectIndex = 0; rectIndex < numRects; rectIndex++) {
1460
+ const x = operations[i + 0];
1461
+ const y = operations[i + 1];
1462
+ const width = operations[i + 2];
1463
+ const height = operations[i + 3];
1464
+ rects.push({x, y, width, height});
1465
+ i += 4;
1466
+ }
1467
+ }
1468
+
1469
if (__DEBUG__) {
1470
debug('Suspense Add', `node ${id} as child of ${parentID}`);
1471
}
@@ -1476,10 +1494,9 @@ export default class Store extends EventEmitter<{
1494
parentID,
1495
children: [],
1496
name,
1497
+ rects,
1498
});
1499
1481
- i += 4;
1482
-
1500
hasSuspenseTreeChanged = true;
1501
break;
1502
}
@@ -1591,6 +1608,61 @@ export default class Store extends EventEmitter<{
1608
hasSuspenseTreeChanged = true;
1609
break;
1610
}
1611
+ case SUSPENSE_TREE_OPERATION_RESIZE: {
1612
+ const id = ((operations[i + 1]: any): number);
1613
+ const numRects = ((operations[i + 2]: any): number);
1614
+ i += 3;
1615
+
1616
+ const suspense = this._idToSuspense.get(id);
1617
+ if (suspense === undefined) {
1618
+ this._throwAndEmitError(
1619
+ Error(
1620
+ `Cannot set rects for suspense node "${id}" because no matching node was found in the Store.`,
1621
+ ),
1622
+ );
1623
+
1624
+ break;
1625
+ }
1626
+
1627
+ let nextRects: SuspenseNode['rects'];
1628
+ if (numRects === -1) {
1629
+ nextRects = null;
1630
+ } else {
1631
+ nextRects = [];
1632
+ for (let rectIndex = 0; rectIndex < numRects; rectIndex++) {
1633
+ const x = operations[i + 0];
1634
+ const y = operations[i + 1];
1635
+ const width = operations[i + 2];
1636
+ const height = operations[i + 3];
1637
+
1638
+ nextRects.push({x, y, width, height});
1639
+
1640
+ i += 4;
1641
+ }
1642
+ }
1643
+
1644
+ suspense.rects = nextRects;
1645
+
1646
+ if (__DEBUG__) {
1647
+ debug(
1648
+ 'Resize',
1649
+ `Suspense node ${id} resize to ${
1650
+ nextRects === null
1651
+ ? 'null'
1652
+ : nextRects
1653
+ .map(
1654
+ rect =>
1655
+ `(${rect.x},${rect.y},${rect.width},${rect.height})`,
1656
+ )
1657
+ .join(',')
1658
+ }`,
1659
+ );
1660
+ }
1661
+
1662
+ hasSuspenseTreeChanged = true;
1663
+
1664
+ break;
1665
+ }
1666
default:
1667
this._throwAndEmitError(
1668
new UnsupportedBridgeOperationError(
packages/react-devtools-shared/src/devtools/utils.js
+69
-2
@@ -10,7 +10,10 @@
10
import JSON5 from 'json5';
11
12
import type {ReactFunctionLocation} from 'shared/ReactTypes';
13
-import type {Element} from 'react-devtools-shared/src/frontend/types';
13
+import type {
14
+ Element,
15
+ SuspenseNode,
16
+} from 'react-devtools-shared/src/frontend/types';
17
import type {StateContext} from './views/Components/TreeContext';
18
import type Store from './store';
19
@@ -28,6 +31,11 @@ export function printElement(
31
key = ` key="${element.key}"`;
32
}
33
34
+ let name = '';
35
+ if (element.nameProp !== null) {
36
+ name = ` name="${element.nameProp}"`;
37
+ }
38
+
39
let hocDisplayNames = null;
40
if (element.hocDisplayNames !== null) {
41
hocDisplayNames = [...element.hocDisplayNames];
@@ -43,7 +51,45 @@ export function printElement(
51
52
return `${' '.repeat(element.depth + 1)}${prefix} <${
53
element.displayName || 'null'
46
- }${key}>${hocs}${suffix}`;
54
+ }${key}${name}>${hocs}${suffix}`;
55
+}
56
+
57
+function printSuspense(
58
+ suspense: SuspenseNode,
59
+ includeWeight: boolean = false,
60
+): string {
61
+ let name = '';
62
+ if (suspense.name !== null) {
63
+ name = ` name="${suspense.name}"`;
64
+ }
65
+
66
+ let printedRects = '';
67
+ const rects = suspense.rects;
68
+ if (rects === null) {
69
+ printedRects = ' rects={null}';
70
+ } else {
71
+ printedRects = ` rects={[${rects.map(rect => `{x:${rect.x},y:${rect.y},width:${rect.width},height:${rect.height}}`).join(', ')}]}`;
72
+ }
73
+
74
+ return `<Suspense${name}${printedRects}>`;
75
+}
76
+
77
+function printSuspenseWithChildren(
78
+ store: Store,
79
+ suspense: SuspenseNode,
80
+ depth: number,
81
+): Array<string> {
82
+ const lines = [' '.repeat(depth) + printSuspense(suspense)];
83
+ for (let i = 0; i < suspense.children.length; i++) {
84
+ const childID = suspense.children[i];
85
+ const child = store.getSuspenseByID(childID);
86
+ if (child === null) {
87
+ throw new Error(`Could not find Suspense node with ID "${childID}".`);
88
+ }
89
+ lines.push(...printSuspenseWithChildren(store, child, depth + 1));
90
+ }
91
+
92
+ return lines;
93
}
94
95
export function printOwnersList(
@@ -59,6 +105,7 @@ export function printStore(
105
store: Store,
106
includeWeight: boolean = false,
107
state: StateContext | null = null,
108
+ includeSuspense: boolean = true,
109
): string {
110
const snapshotLines = [];
111
@@ -129,6 +176,26 @@ export function printStore(
176
}
177
178
rootWeight += weight;
179
+
180
+ if (includeSuspense) {
181
+ const shell = store.getSuspenseByID(rootID);
182
+ // Roots from legacy renderers don't have a separate Suspense tree
183
+ if (shell !== null) {
184
+ if (shell.children.length > 0) {
185
+ snapshotLines.push('[shell]');
186
+ for (let i = 0; i < shell.children.length; i++) {
187
+ const childID = shell.children[i];
188
+ const child = store.getSuspenseByID(childID);
189
+ if (child === null) {
190
+ throw new Error(
191
+ `Could not find Suspense node with ID "${childID}".`,
192
+ );
193
+ }
194
+ snapshotLines.push(...printSuspenseWithChildren(store, child, 1));
195
+ }
196
+ }
197
+ }
198
+ }
199
});
200
201
// Make sure the pretty-printed test align with the Store's reported number of total rows.
packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js
+38
-3
@@ -19,6 +19,7 @@ import {
19
SUSPENSE_TREE_OPERATION_ADD,
20
SUSPENSE_TREE_OPERATION_REMOVE,
21
SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
22
+ SUSPENSE_TREE_OPERATION_RESIZE,
23
} from 'react-devtools-shared/src/constants';
24
import {
25
parseElementDisplayNameFromBackend,
@@ -376,16 +377,26 @@ function updateTree(
377
const fiberID = operations[i + 1];
378
const parentID = operations[i + 2];
379
const nameStringID = operations[i + 3];
380
+ const numRects = operations[i + 4];
381
const name = stringTable[nameStringID];
382
381
- i += 4;
382
-
383
if (__DEBUG__) {
384
+ let rects: string;
385
+ if (numRects === -1) {
386
+ rects = 'null';
387
+ } else {
388
+ rects =
389
+ '[' +
390
+ operations.slice(i + 5, i + 5 + numRects * 4).join(',') +
391
+ ']';
392
+ }
393
debug(
394
'Add suspense',
386
- `node ${fiberID} (${String(name)}) under ${parentID}`,
395
+ `node ${fiberID} (name=${JSON.stringify(name)}, rects={${rects}}) under ${parentID}`,
396
);
397
}
398
+
399
+ i += 5 + (numRects === -1 ? 0 : numRects * 4);
400
break;
401
}
402
@@ -416,6 +427,30 @@ function updateTree(
427
break;
428
}
429
430
+ case SUSPENSE_TREE_OPERATION_RESIZE: {
431
+ const suspenseID = ((operations[i + 1]: any): number);
432
+ const numRects = ((operations[i + 2]: any): number);
433
+
434
+ if (__DEBUG__) {
435
+ if (numRects === -1) {
436
+ debug('Suspense resize', `suspense ${suspenseID} rects null`);
437
+ } else {
438
+ const rects = ((operations.slice(
439
+ i + 3,
440
+ i + 3 + numRects * 4,
441
+ ): any): Array<number>);
442
+ debug(
443
+ 'Suspense resize',
444
+ `suspense ${suspenseID} rects [${rects.join(',')}]`,
445
+ );
446
+ }
447
+ }
448
+
449
+ i += 3 + (numRects === -1 ? 0 : numRects * 4);
450
+
451
+ break;
452
+ }
453
+
454
default:
455
throw Error(`Unsupported Bridge operation "${operation}"`);
456
}
packages/react-devtools-shared/src/frontend/types.js
+8
@@ -185,11 +185,19 @@ export type Element = {
185
compiledWithForget: boolean,
186
};
187
188
+export type Rect = {
189
+ x: number,
190
+ y: number,
191
+ width: number,
192
+ height: number,
193
+};
194
+
195
export type SuspenseNode = {
196
id: Element['id'],
197
parentID: SuspenseNode['id'] | 0,
198
children: Array<SuspenseNode['id']>,
199
name: string | null,
200
+ rects: null | Array<Rect>,
201
};
202
203
// Serialized version of ReactIOInfo
packages/react-devtools-shared/src/utils.js
+53
-2
@@ -43,6 +43,7 @@ import {
43
SUSPENSE_TREE_OPERATION_ADD,
44
SUSPENSE_TREE_OPERATION_REMOVE,
45
SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
46
+ SUSPENSE_TREE_OPERATION_RESIZE,
47
} from './constants';
48
import {
49
ComponentFilterElementType,
@@ -339,11 +340,34 @@ export function printOperationsArray(operations: Array<number>) {
340
const parentID = operations[i + 2];
341
const nameStringID = operations[i + 3];
342
const name = stringTable[nameStringID];
343
+ const numRects = operations[i + 4];
344
343
- i += 4;
345
+ i += 5;
346
+
347
+ let rects: string;
348
+ if (numRects === -1) {
349
+ rects = 'null';
350
+ } else {
351
+ rects = '[';
352
+ for (let rectIndex = 0; rectIndex < numRects; rectIndex++) {
353
+ const offset = i + rectIndex * 4;
354
+ const x = operations[offset + 0];
355
+ const y = operations[offset + 1];
356
+ const width = operations[offset + 2];
357
+ const height = operations[offset + 3];
358
+
359
+ if (rectIndex > 0) {
360
+ rects += ', ';
361
+ }
362
+ rects += `(${x}, ${y}, ${width}, ${height})`;
363
+
364
+ i += 4;
365
+ }
366
+ rects += ']';
367
+ }
368
369
logs.push(
346
- `Add suspense node ${fiberID} (${String(name)}) under ${parentID}`,
370
+ `Add suspense node ${fiberID} (${String(name)},rects={${rects}}) under ${parentID}`,
371
);
372
break;
373
}
@@ -372,6 +396,33 @@ export function printOperationsArray(operations: Array<number>) {
396
);
397
break;
398
}
399
+ case SUSPENSE_TREE_OPERATION_RESIZE: {
400
+ const id = ((operations[i + 1]: any): number);
401
+ const numRects = ((operations[i + 2]: any): number);
402
+ i += 3;
403
+
404
+ if (numRects === -1) {
405
+ logs.push(`Resize suspense node ${id} to null`);
406
+ } else {
407
+ let line = `Resize suspense node ${id} to [`;
408
+ for (let rectIndex = 0; rectIndex < numRects; rectIndex++) {
409
+ const x = operations[i + 0];
410
+ const y = operations[i + 1];
411
+ const width = operations[i + 2];
412
+ const height = operations[i + 3];
413
+
414
+ if (rectIndex > 0) {
415
+ line += ', ';
416
+ }
417
+ line += `(${x}, ${y}, ${width}, ${height})`;
418
+
419
+ i += 4;
420
+ }
421
+ logs.push(line + ']');
422
+ }
423
+
424
+ break;
425
+ }
426
default:
427
throw Error(`Unsupported Bridge operation "${operation}"`);
428
}