@@ -64,28 +64,34 @@ describe('ReactFlightDOM', () => {
64
64
} ;
65
65
}
66
66
67
- function block ( render , load ) {
67
+ function moduleReference ( moduleExport ) {
68
68
const idx = webpackModuleIdx ++ ;
69
69
webpackModules [ idx ] = {
70
- d : render ,
70
+ d : moduleExport ,
71
71
} ;
72
72
webpackMap [ 'path/' + idx ] = {
73
73
id : '' + idx ,
74
74
chunks : [ ] ,
75
75
name : 'd' ,
76
76
} ;
77
+ const MODULE_TAG = Symbol . for ( 'react.module.reference' ) ;
78
+ return { $$typeof : MODULE_TAG , name : 'path/' + idx } ;
79
+ }
80
+
81
+ function block ( render , load ) {
77
82
if ( load === undefined ) {
78
83
return ( ) => {
79
- return ReactTransportDOMServerRuntime . serverBlockNoData ( 'path/' + idx ) ;
84
+ return ReactTransportDOMServerRuntime . serverBlockNoData (
85
+ moduleReference ( render ) ,
86
+ ) ;
80
87
} ;
81
88
}
82
89
return function ( ...args ) {
83
90
const curriedLoad = ( ) => {
84
91
return load ( ...args ) ;
85
92
} ;
86
- const MODULE_TAG = Symbol . for ( 'react.module.reference' ) ;
87
93
return ReactTransportDOMServerRuntime . serverBlock (
88
- { $$typeof : MODULE_TAG , name : 'path/' + idx } ,
94
+ moduleReference ( render ) ,
89
95
curriedLoad ,
90
96
) ;
91
97
} ;
@@ -314,6 +320,9 @@ describe('ReactFlightDOM', () => {
314
320
return 'data' ;
315
321
}
316
322
function DelayedText ( { children} , data ) {
323
+ if ( data !== 'data' ) {
324
+ throw new Error ( 'No data' ) ;
325
+ }
317
326
return < Text > { children } </ Text > ;
318
327
}
319
328
const loadBlock = block ( DelayedText , load ) ;
@@ -477,4 +486,196 @@ describe('ReactFlightDOM', () => {
477
486
'<p>Game over</p>' , // TODO: should not have message in prod.
478
487
) ;
479
488
} ) ;
489
+
490
+ // @gate experimental
491
+ it ( 'should progressively reveal server components' , async ( ) => {
492
+ const { Suspense} = React ;
493
+
494
+ // Client Components
495
+
496
+ class ErrorBoundary extends React . Component {
497
+ state = { hasError : false , error : null } ;
498
+ static getDerivedStateFromError ( error ) {
499
+ return {
500
+ hasError : true ,
501
+ error,
502
+ } ;
503
+ }
504
+ render ( ) {
505
+ if ( this . state . hasError ) {
506
+ return this . props . fallback ( this . state . error ) ;
507
+ }
508
+ return this . props . children ;
509
+ }
510
+ }
511
+
512
+ function MyErrorBoundary ( { children} ) {
513
+ return (
514
+ < ErrorBoundary fallback = { e => < p > { e . message } </ p > } >
515
+ { children }
516
+ </ ErrorBoundary >
517
+ ) ;
518
+ }
519
+
520
+ function Placeholder ( { children, fallback} ) {
521
+ return < Suspense fallback = { fallback } > { children } </ Suspense > ;
522
+ }
523
+
524
+ // Model
525
+ function Text ( { children} ) {
526
+ return children ;
527
+ }
528
+
529
+ function makeDelayedText ( ) {
530
+ let error , _resolve , _reject ;
531
+ let promise = new Promise ( ( resolve , reject ) => {
532
+ _resolve = ( ) => {
533
+ promise = null ;
534
+ resolve ( ) ;
535
+ } ;
536
+ _reject = e => {
537
+ error = e ;
538
+ promise = null ;
539
+ reject ( e ) ;
540
+ } ;
541
+ } ) ;
542
+ function DelayedText ( { children} , data ) {
543
+ if ( promise ) {
544
+ throw promise ;
545
+ }
546
+ if ( error ) {
547
+ throw error ;
548
+ }
549
+ return < Text > { children } </ Text > ;
550
+ }
551
+ return [ DelayedText , _resolve , _reject ] ;
552
+ }
553
+
554
+ const [ Friends , resolveFriends ] = makeDelayedText ( ) ;
555
+ const [ Name , resolveName ] = makeDelayedText ( ) ;
556
+ const [ Posts , resolvePosts ] = makeDelayedText ( ) ;
557
+ const [ Photos , resolvePhotos ] = makeDelayedText ( ) ;
558
+ const [ Games , , rejectGames ] = makeDelayedText ( ) ;
559
+
560
+ // View
561
+ function ProfileDetails ( { avatar} ) {
562
+ return (
563
+ < div >
564
+ < Name > :name:</ Name >
565
+ { avatar }
566
+ </ div >
567
+ ) ;
568
+ }
569
+ function ProfileSidebar ( { friends} ) {
570
+ return (
571
+ < div >
572
+ < Photos > :photos:</ Photos >
573
+ { friends }
574
+ </ div >
575
+ ) ;
576
+ }
577
+ function ProfilePosts ( { posts} ) {
578
+ return < div > { posts } </ div > ;
579
+ }
580
+ function ProfileGames ( { games} ) {
581
+ return < div > { games } </ div > ;
582
+ }
583
+
584
+ const MyErrorBoundaryClient = moduleReference ( MyErrorBoundary ) ;
585
+ const PlaceholderClient = moduleReference ( Placeholder ) ;
586
+
587
+ function ProfileContent ( ) {
588
+ return (
589
+ < >
590
+ < ProfileDetails avatar = { < Text > :avatar:</ Text > } />
591
+ < PlaceholderClient fallback = { < p > (loading sidebar)</ p > } >
592
+ < ProfileSidebar friends = { < Friends > :friends:</ Friends > } />
593
+ </ PlaceholderClient >
594
+ < PlaceholderClient fallback = { < p > (loading posts)</ p > } >
595
+ < ProfilePosts posts = { < Posts > :posts:</ Posts > } />
596
+ </ PlaceholderClient >
597
+ < MyErrorBoundaryClient >
598
+ < PlaceholderClient fallback = { < p > (loading games)</ p > } >
599
+ < ProfileGames games = { < Games > :games:</ Games > } />
600
+ </ PlaceholderClient >
601
+ </ MyErrorBoundaryClient >
602
+ </ >
603
+ ) ;
604
+ }
605
+
606
+ const model = {
607
+ rootContent : < ProfileContent /> ,
608
+ } ;
609
+
610
+ function ProfilePage ( { response} ) {
611
+ return response . readRoot ( ) . rootContent ;
612
+ }
613
+
614
+ const { writable, readable} = getTestStream ( ) ;
615
+ ReactTransportDOMServer . pipeToNodeWritable ( model , writable , webpackMap ) ;
616
+ const response = ReactTransportDOMClient . createFromReadableStream ( readable ) ;
617
+
618
+ const container = document . createElement ( 'div' ) ;
619
+ const root = ReactDOM . unstable_createRoot ( container ) ;
620
+ await act ( async ( ) => {
621
+ root . render (
622
+ < Suspense fallback = { < p > (loading)</ p > } >
623
+ < ProfilePage response = { response } />
624
+ </ Suspense > ,
625
+ ) ;
626
+ } ) ;
627
+ expect ( container . innerHTML ) . toBe ( '<p>(loading)</p>' ) ;
628
+
629
+ // This isn't enough to show anything.
630
+ await act ( async ( ) => {
631
+ resolveFriends ( ) ;
632
+ } ) ;
633
+ expect ( container . innerHTML ) . toBe ( '<p>(loading)</p>' ) ;
634
+
635
+ // We can now show the details. Sidebar and posts are still loading.
636
+ await act ( async ( ) => {
637
+ resolveName ( ) ;
638
+ } ) ;
639
+ // Advance time enough to trigger a nested fallback.
640
+ jest . advanceTimersByTime ( 500 ) ;
641
+ expect ( container . innerHTML ) . toBe (
642
+ '<div>:name::avatar:</div>' +
643
+ '<p>(loading sidebar)</p>' +
644
+ '<p>(loading posts)</p>' +
645
+ '<p>(loading games)</p>' ,
646
+ ) ;
647
+
648
+ // Let's *fail* loading games.
649
+ await act ( async ( ) => {
650
+ rejectGames ( new Error ( 'Game over' ) ) ;
651
+ } ) ;
652
+ expect ( container . innerHTML ) . toBe (
653
+ '<div>:name::avatar:</div>' +
654
+ '<p>(loading sidebar)</p>' +
655
+ '<p>(loading posts)</p>' +
656
+ '<p>Game over</p>' , // TODO: should not have message in prod.
657
+ ) ;
658
+
659
+ // We can now show the sidebar.
660
+ await act ( async ( ) => {
661
+ resolvePhotos ( ) ;
662
+ } ) ;
663
+ expect ( container . innerHTML ) . toBe (
664
+ '<div>:name::avatar:</div>' +
665
+ '<div>:photos::friends:</div>' +
666
+ '<p>(loading posts)</p>' +
667
+ '<p>Game over</p>' , // TODO: should not have message in prod.
668
+ ) ;
669
+
670
+ // Show everything.
671
+ await act ( async ( ) => {
672
+ resolvePosts ( ) ;
673
+ } ) ;
674
+ expect ( container . innerHTML ) . toBe (
675
+ '<div>:name::avatar:</div>' +
676
+ '<div>:photos::friends:</div>' +
677
+ '<div>:posts:</div>' +
678
+ '<p>Game over</p>' , // TODO: should not have message in prod.
679
+ ) ;
680
+ } ) ;
480
681
} ) ;
0 commit comments