@@ -656,4 +656,121 @@ describe('ReactDOMFizzShellHydration', () => {
656
656
expect ( container . innerHTML ) . toBe ( 'Client' ) ;
657
657
} ,
658
658
) ;
659
+
660
+ it ( 'handles conditional use with a cascading update and error boundaries' , async ( ) => {
661
+ class ErrorBoundary extends React . Component {
662
+ constructor ( props ) {
663
+ super ( props ) ;
664
+ this . state = { error : null } ;
665
+ }
666
+
667
+ static getDerivedStateFromError ( error ) {
668
+ return { error} ;
669
+ }
670
+
671
+ componentDidCatch ( ) { }
672
+
673
+ render ( ) {
674
+ if ( this . state . error ) {
675
+ return 'Something went wrong: ' + this . state . error . message ;
676
+ }
677
+
678
+ return this . props . children ;
679
+ }
680
+ }
681
+
682
+ function Bomb ( ) {
683
+ throw new Error ( 'boom' ) ;
684
+ }
685
+
686
+ function Updater ( { setPromise} ) {
687
+ const [ state , setState ] = React . useState ( false ) ;
688
+
689
+ React . useEffect ( ( ) => {
690
+ // deleting this set state removes too many hooks error
691
+ setState ( true ) ;
692
+ // deleting this startTransition removes too many hooks error
693
+ startTransition ( ( ) => {
694
+ setPromise ( Promise . resolve ( 'resolved' ) ) ;
695
+ } ) ;
696
+ } , [ state ] ) ;
697
+
698
+ return null ;
699
+ }
700
+
701
+ function Page ( ) {
702
+ const [ promise , setPromise ] = React . useState ( null ) ;
703
+ Scheduler . log ( 'use: ' + promise ) ;
704
+ /**
705
+ * this part is tricky, I cannot say confidently the conditional `use` is required for the reproduction.
706
+ * If we tried to run use(promise ?? cachedPromise) we wouldn't be able renderToString without a parent suspense boundary
707
+ * but with a parent suspense the bug is no longer reproducible (with or without conditional use)
708
+ * and without renderToString + hydration, the bug is no longer reproducible
709
+ */
710
+ const value = promise ? React . use ( promise ) : promise ;
711
+ Scheduler . log ( 'used: ' + value ) ;
712
+
713
+ React . useMemo ( ( ) => { } , [ ] ) ; // to trigger too many hooks error
714
+ return (
715
+ < >
716
+ < Updater setPromise = { setPromise } />
717
+ < React . Suspense fallback = "Loading..." >
718
+ < ErrorBoundary >
719
+ < Bomb />
720
+ </ ErrorBoundary >
721
+ </ React . Suspense >
722
+ hello world
723
+ </ >
724
+ ) ;
725
+ }
726
+ function App ( ) {
727
+ return < Page /> ;
728
+ }
729
+
730
+ // Server render
731
+ await serverAct ( async ( ) => {
732
+ const { pipe} = ReactDOMFizzServer . renderToPipeableStream ( < App /> , {
733
+ onError ( error ) {
734
+ Scheduler . log ( 'onError: ' + error . message ) ;
735
+ } ,
736
+ } ) ;
737
+ pipe ( writable ) ;
738
+ } ) ;
739
+ assertLog ( [ 'use: null' , 'used: null' , 'onError: boom' ] ) ;
740
+
741
+ expect ( container . textContent ) . toBe ( 'Loading...hello world' ) ;
742
+
743
+ await clientAct ( async ( ) => {
744
+ ReactDOMClient . hydrateRoot ( container , < App /> , {
745
+ onCaughtError ( error ) {
746
+ Scheduler . log ( 'onCaughtError: ' + error . message ) ;
747
+ } ,
748
+ onUncaughtError ( error ) {
749
+ Scheduler . log ( 'onUncaughtError: ' + error . message ) ;
750
+ } ,
751
+ onRecoverableError ( error ) {
752
+ Scheduler . log ( 'onRecoverableError: ' + error . message ) ;
753
+ if ( error . cause ) {
754
+ Scheduler . log ( 'Cause: ' + error . cause . message ) ;
755
+ }
756
+ } ,
757
+ } ) ;
758
+ } ) ;
759
+ assertLog ( [
760
+ 'use: null' ,
761
+ 'used: null' ,
762
+ 'use: [object Promise]' ,
763
+ 'onCaughtError: boom' ,
764
+ 'use: [object Promise]' ,
765
+ 'use: [object Promise]' ,
766
+ 'used: resolved' ,
767
+ 'use: [object Promise]' ,
768
+ 'used: resolved' ,
769
+ // FIXME
770
+ 'onUncaughtError: Rendered more hooks than during the previous render.' ,
771
+ ] ) ;
772
+
773
+ // Should've rendered something. The error was handled by the error boundary.
774
+ expect ( container . textContent ) . toBe ( '' ) ;
775
+ } ) ;
659
776
} ) ;
0 commit comments