[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__) {