101
}
102
}
103
104
+ async function hydrateRootAndCollectErrors(reactNode) {
105
+ const errors = [];
106
+ await clientAct(async () => {
107
+ ReactDOMClient.hydrateRoot(container, reactNode, {
108
+ onCaughtError(error) {
109
+ Scheduler.log('onCaughtError: ' + error.message);
110
+ errors.push('caught: ' + error.message);
111
+ },
112
+ onUncaughtError(error) {
113
+ Scheduler.log('onUncaughtError: ' + error.message);
114
+ errors.push('uncaught: ' + error.message);
115
+ },
116
+ onRecoverableError(error) {
117
+ Scheduler.log('onRecoverableError: ' + error.message);
118
+ errors.push('recoverable: ' + error.message);
119
+ },
120
+ });
121
+ });
122
+ return errors;
123
+ }
124
+
125
+ function createErrorBoundaryAndBomb() {
126
+ class ErrorBoundary extends React.Component {
127
+ constructor(props) {
128
+ super(props);
129
+ this.state = {error: null};
130
+ }
131
+
132
+ static getDerivedStateFromError(error) {
133
+ return {error};
134
+ }
135
+
136
+ componentDidCatch() {}
137
+
138
+ render() {
139
+ if (this.state.error) {
140
+ return 'Something went wrong: ' + this.state.error.message;
141
+ }
142
+
143
+ return this.props.children;
144
+ }
145
+ }
146
+
147
+ function Bomb() {
148
+ throw new Error('boom');
149
+ }
150
+
151
+ return {ErrorBoundary, Bomb};
152
+ }
153
+
154
function resolveText(text) {
155
const record = textCache.get(text);
156
if (record === undefined) {
705
expect(container.innerHTML).toBe('Client');
706
},
707
);
708
+
709
+ it(
710
+ 'does not corrupt hooks during hydration when conditional use suspends ' +
711
+ 'after a cascading update (#33580)',
712
+ async () => {
713
+ const {ErrorBoundary, Bomb} = createErrorBoundaryAndBomb();
714
+
715
+ function Updater({setPromise}) {
716
+ const [state, setState] = React.useState(false);
717
+
718
+ React.useEffect(() => {
719
+ setState(true);
720
+ startTransition(() => {
721
+ setPromise(Promise.resolve('resolved'));
722
+ });
723
+ }, [state]);
724
+
725
+ return null;
726
+ }
727
+
728
+ function Page() {
729
+ const [promise, setPromise] = React.useState(null);
730
+ const value = promise ? React.use(promise) : promise;
731
+
732
+ React.useMemo(() => {}, []);
733
+
734
+ return (
735
+ <>
736
+ <Updater setPromise={setPromise} />
737
+ <React.Suspense fallback="Loading...">
738
+ <ErrorBoundary>
739
+ <Bomb />
740
+ </ErrorBoundary>
741
+ </React.Suspense>
742
+ {value !== null ? value : 'hello world'}
743
+ </>
744
+ );
745
+ }
746
+
747
+ function App() {
748
+ return <Page />;
749
+ }
750
+
751
+ await serverAct(async () => {
752
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />, {
753
+ onError(error) {
754
+ Scheduler.log('onError: ' + error.message);
755
+ },
756
+ });
757
+ pipe(writable);
758
+ });
759
+ assertLog(['onError: boom']);
760
+
761
+ const errors = await hydrateRootAndCollectErrors(<App />);
762
+ assertLog(['onCaughtError: boom']);
763
+
764
+ expect(
765
+ errors.find(error => error.includes('Rendered more hooks')),
766
+ ).toBeUndefined();
767
+ expect(container.textContent).toBe('Something went wrong: boomresolved');
768
+ },
769
+ );
770
+
771
+ it('preserves hooks when suspension happens before the first tracked hook', async () => {
772
+ const {ErrorBoundary, Bomb} = createErrorBoundaryAndBomb();
773
+ let setReady;
774
+
775
+ function Updater({setPromise}) {
776
+ React.useEffect(() => {
777
+ setReady(true);
778
+ startTransition(() => {
779
+ setPromise(Promise.resolve('resolved'));
780
+ });
781
+ }, []);
782
+
783
+ return null;
784
+ }
785
+
786
+ function Page({promise}) {
787
+ const value = promise ? React.use(promise) : promise;
788
+
789
+ const [ready, _setReady] = React.useState(false);
790
+ setReady = _setReady;
791
+
792
+ React.useMemo(() => {}, []);
793
+
794
+ return (
795
+ <>
796
+ <React.Suspense fallback="Loading...">
797
+ <ErrorBoundary>
798
+ <Bomb />
799
+ </ErrorBoundary>
800
+ </React.Suspense>
801
+ <span>{ready ? 'ready' : 'not-ready'}</span>
802
+ <span>{value !== null ? value : 'hello world'}</span>
803
+ </>
804
+ );
805
+ }
806
+
807
+ function App() {
808
+ const [promise, setPromise] = React.useState(null);
809
+
810
+ return (
811
+ <>
812
+ <Updater setPromise={setPromise} />
813
+ <Page promise={promise} />
814
+ </>
815
+ );
816
+ }
817
+
818
+ await serverAct(async () => {
819
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />, {
820
+ onError(error) {
821
+ Scheduler.log('onError: ' + error.message);
822
+ },
823
+ });
824
+ pipe(writable);
825
+ });
826
+ assertLog(['onError: boom']);
827
+
828
+ const errors = await hydrateRootAndCollectErrors(<App />);
829
+ assertLog(['onCaughtError: boom']);
830
+
831
+ expect(
832
+ errors.find(error => error.includes('Rendered more hooks')),
833
+ ).toBeUndefined();
834
+ expect(container.textContent).toBe(
835
+ 'Something went wrong: boomreadyresolved',
836
+ );
837
+ });
838
});