@samitouri / QOS-React-2 / commits / 5a06072780

[Old server renderer] Retry error on client (#22399)

We're still in the process of migrating to the Fizz server renderer. In the meantime, this makes the error semantics on the old server renderer match the behavior of the new one: if an error is thrown, it triggers a Suspense fallback, just as if it suspended (this part was already implemented). Then the errored tree is retried on the client, where it may recover and finish successfully.

Andrew Clark committed Sep 22, 2021 at 11:45 UTC 5a06072780e1323204cd7823665b3a95a0450747
3 files changed +65 -12
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+28
@@ -2671,4 +2671,32 @@ describe('ReactDOMServerPartialHydration', () => {
2671 expect(ref.current).toBe(span);
2672 expect(ref.current.innerHTML).toBe('Hidden child');
2673 });
2674 +
2675 + it('should retry on client if something throws', async () => {
2676 + let isServer = true;
2677 + function ThrowsOnServerOnly() {
2678 + if (isServer) {
2679 + throw new Error('Oops!');
2680 + }
2681 + return 'Yay!';
2682 + }
2683 +
2684 + function App() {
2685 + return (
2686 + <React.Suspense fallback="Loading...">
2687 + <ThrowsOnServerOnly />
2688 + </React.Suspense>
2689 + );
2690 + }
2691 +
2692 + const element = document.createElement('div');
2693 + element.innerHTML = ReactDOMServer.renderToString(<App />);
2694 + expect(element.textContent).toBe('Loading...');
2695 +
2696 + isServer = false;
2697 + await act(async () => {
2698 + ReactDOM.hydrateRoot(element, <App />);
2699 + });
2700 + expect(element.textContent).toBe('Yay!');
2701 + });
2702 });
packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js
+14
@@ -115,6 +115,20 @@ describe('ReactDOMServerSuspense', () => {
115 expect(getVisibleChildren(c)).toEqual(<div>Fallback</div>);
116 });
117
118 + it('should render the fallback when an error is thrown', async () => {
119 + function Throws() {
120 + throw new Error('Oops!');
121 + }
122 + const c = await serverRender(
123 + <div>
124 + <React.Suspense fallback={<Text text="Fallback" />}>
125 + <Throws />
126 + </React.Suspense>
127 + </div>,
128 + );
129 + expect(getVisibleChildren(c)).toEqual(<div>Fallback</div>);
130 + });
131 +
132 it('should work with nested suspense components', async () => {
133 const c = await serverRender(
134 <div>
packages/react-dom/src/server/ReactPartialRenderer.js
+23 -12
@@ -969,22 +969,33 @@ class ReactDOMServerRenderer {
969 try {
970 outBuffer += this.render(child, frame.context, frame.domNamespace);
971 } catch (err) {
972 - if (err != null && typeof err.then === 'function') {
973 - if (enableSuspenseServerRenderer) {
974 - invariant(
975 - this.suspenseDepth > 0,
976 - // TODO: include component name. This is a bit tricky with current factoring.
977 - 'A React component suspended while rendering, but no fallback UI was specified.\n' +
978 - '\n' +
979 - 'Add a <Suspense fallback=...> component higher in the tree to ' +
980 - 'provide a loading indicator or placeholder to display.',
981 - );
972 + if (enableSuspenseServerRenderer) {
973 + if (this.suspenseDepth > 0) {
974 + // Regardless of whether this is an error or a suspense thenable,
975 + // trigger the nearest Suspense boundary. We'll try to render this
976 + // tree again on the client.
977 suspended = true;
978 } else {
984 - invariant(false, 'ReactDOMServer does not yet support Suspense.');
979 + // Missing a Suspense boundary. This is a fatal error.
980 + if (err != null && typeof err.then === 'function') {
981 + invariant(
982 + false,
983 + // TODO: include component name. This is a bit tricky with current factoring.
984 + 'A React component suspended while rendering, but no fallback UI was specified.\n' +
985 + '\n' +
986 + 'Add a <Suspense fallback=...> component higher in the tree to ' +
987 + 'provide a loading indicator or placeholder to display.',
988 + );
989 + } else {
990 + throw err;
991 + }
992 }
993 } else {
987 - throw err;
994 + if (err != null && typeof err.then === 'function') {
995 + invariant(false, 'ReactDOMServer does not yet support Suspense.');
996 + } else {
997 + throw err;
998 + }
999 }
1000 } finally {
1001 if (__DEV__) {