Support more empty types (#21225)
Undefined errors as a direct return value. This changes semantics for "true" and functions to mirror the client.
Sebastian Markbåge committed
Apr 9, 2021 at 19:23 UTC
b0407b55ff59c60530c85a1ed14b55950d94cb9a
1 file changed
+97
-33
packages/react-server/src/ReactFizzServer.js
+97
-33
@@ -56,7 +56,13 @@ import {
56
processChildContext,
57
emptyContextObject,
58
} from './ReactFizzContext';
59
-import {REACT_ELEMENT_TYPE, REACT_SUSPENSE_TYPE} from 'shared/ReactSymbols';
59
+import {
60
+ getIteratorFn,
61
+ REACT_ELEMENT_TYPE,
62
+ REACT_PORTAL_TYPE,
63
+ REACT_LAZY_TYPE,
64
+ REACT_SUSPENSE_TYPE,
65
+} from 'shared/ReactSymbols';
66
import ReactSharedInternals from 'shared/ReactSharedInternals';
67
import {
68
disableLegacyContext,
@@ -421,6 +427,16 @@ function shouldConstruct(Component) {
427
return Component.prototype && Component.prototype.isReactComponent;
428
}
429
430
+function invalidRenderResult(type: any): void {
431
+ invariant(
432
+ false,
433
+ '%s(...): Nothing was returned from render. This usually means a ' +
434
+ 'return statement is missing. Or, to render nothing, ' +
435
+ 'return null.',
436
+ getComponentNameFromType(type) || 'Component',
437
+ );
438
+}
439
+
440
function renderWithHooks<Props, SecondArg>(
441
request: Request,
442
task: Task,
@@ -430,6 +446,9 @@ function renderWithHooks<Props, SecondArg>(
446
): any {
447
// TODO: Set up Hooks etc.
448
const children = Component(props, secondArg);
449
+ if (children === undefined) {
450
+ invalidRenderResult(Component);
451
+ }
452
return children;
453
}
454
@@ -441,6 +460,13 @@ function finishClassComponent(
460
props: any,
461
): ReactNodeList {
462
const nextChildren = instance.render();
463
+ if (nextChildren === undefined) {
464
+ if (__DEV__ && instance.render._isMockFunction) {
465
+ // We allow auto-mocks to proceed as if they're returning null.
466
+ } else {
467
+ invalidRenderResult(Component);
468
+ }
469
+ }
470
471
if (__DEV__) {
472
if (instance.props !== props) {
@@ -693,6 +719,58 @@ function renderNodeDestructive(
719
// something suspends.
720
task.node = node;
721
722
+ // Handle object types
723
+ if (typeof node === 'object' && node !== null) {
724
+ switch ((node: any).$$typeof) {
725
+ case REACT_ELEMENT_TYPE: {
726
+ const element: React$Element<any> = (node: any);
727
+ const type = element.type;
728
+ const props = element.props;
729
+ renderElement(request, task, type, props, node);
730
+ return;
731
+ }
732
+ case REACT_PORTAL_TYPE:
733
+ throw new Error('Not yet implemented node type.');
734
+ case REACT_LAZY_TYPE:
735
+ throw new Error('Not yet implemented node type.');
736
+ }
737
+
738
+ if (isArray(node)) {
739
+ if (node.length > 0) {
740
+ for (let i = 0; i < node.length; i++) {
741
+ // Recursively render the rest. We need to use the non-destructive form
742
+ // so that we can safely pop back up and render the sibling if something
743
+ // suspends.
744
+ renderNode(request, task, node[i]);
745
+ }
746
+ } else {
747
+ pushEmpty(
748
+ task.blockedSegment.chunks,
749
+ request.responseState,
750
+ task.assignID,
751
+ );
752
+ task.assignID = null;
753
+ }
754
+ return;
755
+ }
756
+
757
+ const iteratorFn = getIteratorFn(node);
758
+ if (iteratorFn) {
759
+ throw new Error('Not yet implemented node type.');
760
+ }
761
+
762
+ const childString = Object.prototype.toString.call(node);
763
+ invariant(
764
+ false,
765
+ 'Objects are not valid as a React child (found: %s). ' +
766
+ 'If you meant to render a collection of children, use an array ' +
767
+ 'instead.',
768
+ childString === '[object Object]'
769
+ ? 'object with keys {' + Object.keys(node).join(', ') + '}'
770
+ : childString,
771
+ );
772
+ }
773
+
774
if (typeof node === 'string') {
775
pushTextInstance(
776
task.blockedSegment.chunks,
@@ -704,43 +782,29 @@ function renderNodeDestructive(
782
return;
783
}
784
707
- if (isArray(node)) {
708
- if (node.length > 0) {
709
- for (let i = 0; i < node.length; i++) {
710
- // Recursively render the rest. We need to use the non-destructive form
711
- // so that we can safely pop back up and render the sibling if something
712
- // suspends.
713
- renderNode(request, task, node[i]);
714
- }
715
- } else {
716
- pushEmpty(
717
- task.blockedSegment.chunks,
718
- request.responseState,
719
- task.assignID,
720
- );
721
- task.assignID = null;
722
- }
723
- return;
724
- }
725
-
726
- if (node === null) {
727
- pushEmpty(task.blockedSegment.chunks, request.responseState, task.assignID);
785
+ if (typeof node === 'number') {
786
+ pushTextInstance(
787
+ task.blockedSegment.chunks,
788
+ '' + node,
789
+ request.responseState,
790
+ task.assignID,
791
+ );
792
+ task.assignID = null;
793
return;
794
}
795
731
- if (
732
- typeof node === 'object' &&
733
- node &&
734
- (node: any).$$typeof === REACT_ELEMENT_TYPE
735
- ) {
736
- const element: React$Element<any> = (node: any);
737
- const type = element.type;
738
- const props = element.props;
739
- renderElement(request, task, type, props, node);
740
- return;
796
+ if (__DEV__) {
797
+ if (typeof node === 'function') {
798
+ console.error(
799
+ 'Functions are not valid as a React child. This may happen if ' +
800
+ 'you return a Component instead of <Component /> from render. ' +
801
+ 'Or maybe you meant to call this function rather than return it.',
802
+ );
803
+ }
804
}
805
743
- throw new Error('Not yet implemented node type.');
806
+ // Any other type is assumed to be empty.
807
+ pushEmpty(task.blockedSegment.chunks, request.responseState, task.assignID);
808
}
809
810
function spawnNewSuspendedTask(