@@ -64,28 +64,34 @@ describe('ReactFlightDOM', () => {
6464 } ;
6565 }
6666
67- function block ( render , load ) {
67+ function moduleReference ( moduleExport ) {
6868 const idx = webpackModuleIdx ++ ;
6969 webpackModules [ idx ] = {
70- d : render ,
70+ d : moduleExport ,
7171 } ;
7272 webpackMap [ 'path/' + idx ] = {
7373 id : '' + idx ,
7474 chunks : [ ] ,
7575 name : 'd' ,
7676 } ;
77+ const MODULE_TAG = Symbol . for ( 'react.module.reference' ) ;
78+ return { $$typeof : MODULE_TAG , name : 'path/' + idx } ;
79+ }
80+
81+ function block ( render , load ) {
7782 if ( load === undefined ) {
7883 return ( ) => {
79- return ReactTransportDOMServerRuntime . serverBlockNoData ( 'path/' + idx ) ;
84+ return ReactTransportDOMServerRuntime . serverBlockNoData (
85+ moduleReference ( render ) ,
86+ ) ;
8087 } ;
8188 }
8289 return function ( ...args ) {
8390 const curriedLoad = ( ) => {
8491 return load ( ...args ) ;
8592 } ;
86- const MODULE_TAG = Symbol . for ( 'react.module.reference' ) ;
8793 return ReactTransportDOMServerRuntime . serverBlock (
88- { $$typeof : MODULE_TAG , name : 'path/' + idx } ,
94+ moduleReference ( render ) ,
8995 curriedLoad ,
9096 ) ;
9197 } ;
@@ -314,6 +320,9 @@ describe('ReactFlightDOM', () => {
314320 return 'data' ;
315321 }
316322 function DelayedText ( { children} , data ) {
323+ if ( data !== 'data' ) {
324+ throw new Error ( 'No data' ) ;
325+ }
317326 return < Text > { children } </ Text > ;
318327 }
319328 const loadBlock = block ( DelayedText , load ) ;
@@ -477,4 +486,196 @@ describe('ReactFlightDOM', () => {
477486 '<p>Game over</p>' , // TODO: should not have message in prod.
478487 ) ;
479488 } ) ;
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+ } ) ;
480681} ) ;
0 commit comments