Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 56f93a7

Browse files
authored
Throw on unhandled SSR suspending (facebook#16460)
* Throw on unhandled SSR suspending * Add a nicer message when the flag is off * Tweak internal refinement error message
1 parent dce430a commit 56f93a7

File tree

4 files changed

+170
-8
lines changed

4 files changed

+170
-8
lines changed

packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,11 @@ function initModules() {
3737
};
3838
}
3939

40-
const {resetModules, serverRender} = ReactDOMServerIntegrationUtils(
41-
initModules,
42-
);
40+
const {
41+
itThrowsWhenRendering,
42+
resetModules,
43+
serverRender,
44+
} = ReactDOMServerIntegrationUtils(initModules);
4345

4446
describe('ReactDOMServerSuspense', () => {
4547
beforeEach(() => {
@@ -133,4 +135,86 @@ describe('ReactDOMServerSuspense', () => {
133135
expect(divA).toBe(divA2);
134136
expect(divB).toBe(divB2);
135137
});
138+
139+
itThrowsWhenRendering(
140+
'a suspending component outside a Suspense node',
141+
async render => {
142+
await render(
143+
<div>
144+
<React.Suspense />
145+
<AsyncText text="Children" />
146+
<React.Suspense />
147+
</div>,
148+
1,
149+
);
150+
},
151+
'Add a <Suspense fallback=...> component higher in the tree',
152+
);
153+
154+
itThrowsWhenRendering(
155+
'a suspending component without a Suspense above',
156+
async render => {
157+
await render(
158+
<div>
159+
<AsyncText text="Children" />
160+
</div>,
161+
1,
162+
);
163+
},
164+
'Add a <Suspense fallback=...> component higher in the tree',
165+
);
166+
167+
it('does not get confused by throwing null', () => {
168+
function Bad() {
169+
// eslint-disable-next-line no-throw-literal
170+
throw null;
171+
}
172+
173+
let didError;
174+
let error;
175+
try {
176+
ReactDOMServer.renderToString(<Bad />);
177+
} catch (err) {
178+
didError = true;
179+
error = err;
180+
}
181+
expect(didError).toBe(true);
182+
expect(error).toBe(null);
183+
});
184+
185+
it('does not get confused by throwing undefined', () => {
186+
function Bad() {
187+
// eslint-disable-next-line no-throw-literal
188+
throw undefined;
189+
}
190+
191+
let didError;
192+
let error;
193+
try {
194+
ReactDOMServer.renderToString(<Bad />);
195+
} catch (err) {
196+
didError = true;
197+
error = err;
198+
}
199+
expect(didError).toBe(true);
200+
expect(error).toBe(undefined);
201+
});
202+
203+
it('does not get confused by throwing a primitive', () => {
204+
function Bad() {
205+
// eslint-disable-next-line no-throw-literal
206+
throw 'foo';
207+
}
208+
209+
let didError;
210+
let error;
211+
try {
212+
ReactDOMServer.renderToString(<Bad />);
213+
} catch (err) {
214+
didError = true;
215+
error = err;
216+
}
217+
expect(didError).toBe(true);
218+
expect(error).toBe('foo');
219+
});
136220
});

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

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,70 @@ describe('ReactDOMServer', () => {
705705
}).toThrow('ReactDOMServer does not yet support lazy-loaded components.');
706706
});
707707

708+
it('throws when suspending on the server', () => {
709+
function AsyncFoo() {
710+
throw new Promise(() => {});
711+
}
712+
713+
expect(() => {
714+
ReactDOMServer.renderToString(<AsyncFoo />);
715+
}).toThrow('ReactDOMServer does not yet support Suspense.');
716+
});
717+
718+
it('does not get confused by throwing null', () => {
719+
function Bad() {
720+
// eslint-disable-next-line no-throw-literal
721+
throw null;
722+
}
723+
724+
let didError;
725+
let error;
726+
try {
727+
ReactDOMServer.renderToString(<Bad />);
728+
} catch (err) {
729+
didError = true;
730+
error = err;
731+
}
732+
expect(didError).toBe(true);
733+
expect(error).toBe(null);
734+
});
735+
736+
it('does not get confused by throwing undefined', () => {
737+
function Bad() {
738+
// eslint-disable-next-line no-throw-literal
739+
throw undefined;
740+
}
741+
742+
let didError;
743+
let error;
744+
try {
745+
ReactDOMServer.renderToString(<Bad />);
746+
} catch (err) {
747+
didError = true;
748+
error = err;
749+
}
750+
expect(didError).toBe(true);
751+
expect(error).toBe(undefined);
752+
});
753+
754+
it('does not get confused by throwing a primitive', () => {
755+
function Bad() {
756+
// eslint-disable-next-line no-throw-literal
757+
throw 'foo';
758+
}
759+
760+
let didError;
761+
let error;
762+
try {
763+
ReactDOMServer.renderToString(<Bad />);
764+
} catch (err) {
765+
didError = true;
766+
error = err;
767+
}
768+
expect(didError).toBe(true);
769+
expect(error).toBe('foo');
770+
});
771+
708772
it('should throw (in dev) when children are mutated during render', () => {
709773
function Wrapper(props) {
710774
props.children[1] = <p key={1} />; // Mutation is illegal

packages/react-dom/src/server/ReactPartialRenderer.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -883,7 +883,8 @@ class ReactDOMServerRenderer {
883883
const fallbackFrame = frame.fallbackFrame;
884884
invariant(
885885
fallbackFrame,
886-
'suspense fallback not found, something is broken',
886+
'ReactDOMServer did not find an internal fallback frame for Suspense. ' +
887+
'This is a bug in React. Please file an issue.',
887888
);
888889
this.stack.push(fallbackFrame);
889890
out[this.suspenseDepth] += '<!--$!-->';
@@ -909,8 +910,20 @@ class ReactDOMServerRenderer {
909910
try {
910911
outBuffer += this.render(child, frame.context, frame.domNamespace);
911912
} catch (err) {
912-
if (enableSuspenseServerRenderer && typeof err.then === 'function') {
913-
suspended = true;
913+
if (err != null && typeof err.then === 'function') {
914+
if (enableSuspenseServerRenderer) {
915+
invariant(
916+
this.suspenseDepth > 0,
917+
// TODO: include component name. This is a bit tricky with current factoring.
918+
'A React component suspended while rendering, but no fallback UI was specified.\n' +
919+
'\n' +
920+
'Add a <Suspense fallback=...> component higher in the tree to ' +
921+
'provide a loading indicator or placeholder to display.',
922+
);
923+
suspended = true;
924+
} else {
925+
invariant(false, 'ReactDOMServer does not yet support Suspense.');
926+
}
914927
} else {
915928
throw err;
916929
}

scripts/error-codes/codes.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@
301301
"300": "Rendered fewer hooks than expected. This may be caused by an accidental early return statement.",
302302
"301": "Too many re-renders. React limits the number of renders to prevent an infinite loop.",
303303
"302": "It is not supported to run the profiling version of a renderer (for example, `react-dom/profiling`) without also replacing the `scheduler/tracing` module with `scheduler/tracing-profiling`. Your bundler might have a setting for aliasing both modules. Learn more at http://fb.me/react-profiling",
304-
"303": "suspense fallback not found, something is broken",
304+
"303": "ReactDOMServer did not find an internal fallback frame for Suspense. This is a bug in React. Please file an issue.",
305305
"304": "Maximum number of concurrent React renderers exceeded. This can happen if you are not properly destroying the Readable provided by React. Ensure that you call .destroy() on it if you no longer want to read from it, and did not read to the end. If you use .pipe() this should be automatic.",
306306
"305": "The current renderer does not support hydration. This error is likely caused by a bug in React. Please file an issue.",
307307
"306": "Element type is invalid. Received a promise that resolves to: %s. Lazy element type must resolve to a class or function.%s",
@@ -339,5 +339,6 @@
339339
"338": "ReactDOMServer does not yet support the fundamental API.",
340340
"339": "An invalid value was used as an event listener. Expect one or many event listeners created via React.unstable_useResponder().",
341341
"340": "Threw in newly mounted dehydrated component. This is likely a bug in React. Please file an issue.",
342-
"341": "We just came from a parent so we must have had a parent. This is a bug in React."
342+
"341": "We just came from a parent so we must have had a parent. This is a bug in React.",
343+
"342": "A React component suspended while rendering, but no fallback UI was specified.\n\nAdd a <Suspense fallback=...> component higher in the tree to provide a loading indicator or placeholder to display."
343344
}

0 commit comments

Comments
 (0)