Skip to content

Commit 0cf2a10

Browse files
eps1lonRobPruzan
andcommitted
Repro for the "rendered more hooks" bug
Co-authored-by: Rob Pruzan <[email protected]>
1 parent 36c63d4 commit 0cf2a10

File tree

1 file changed

+117
-0
lines changed

1 file changed

+117
-0
lines changed

packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,4 +656,121 @@ describe('ReactDOMFizzShellHydration', () => {
656656
expect(container.innerHTML).toBe('Client');
657657
},
658658
);
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+
});
659776
});

0 commit comments

Comments
 (0)