Skip to content

Commit 63edf30

Browse files
authored
Merge pull request #8797 from acdlite/fix-arrays
[Fiber] disableNewFiberFeatures bugfix: host component children arrays
2 parents a3ff8c3 + ba1d382 commit 63edf30

17 files changed

+204
-138
lines changed

scripts/fiber/tests-passing.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,7 @@ src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js
554554
* should not crash encountering low-priority tree
555555
* throws if non-element passed to top-level render
556556
* throws if something other than false, null, or an element is returned from render
557+
* treats mocked render functions as if they return null
557558

558559
src/renderers/dom/shared/__tests__/CSSProperty-test.js
559560
* should generate browser prefixes for its `isUnitlessNumber`

scripts/jest/test-framework-setup.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ jest.mock('ReactDOMFeatureFlags', () => {
99
useFiber: flags.useFiber || !!process.env.REACT_DOM_JEST_USE_FIBER,
1010
});
1111
});
12+
jest.mock('ReactFeatureFlags', () => {
13+
const flags = require.requireActual('ReactFeatureFlags');
14+
return Object.assign({}, flags, {
15+
disableNewFiberFeatures: true,
16+
});
17+
});
1218

1319
// Error logging varies between Fiber and Stack;
1420
// Rather than fork dozens of tests, mock the error-logging file by default.

src/renderers/dom/fiber/ReactDOMFiber.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type { ReactNodeList } from 'ReactTypes';
1818
var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
1919
var ReactControlledComponent = require('ReactControlledComponent');
2020
var ReactDOMComponentTree = require('ReactDOMComponentTree');
21+
var ReactFeatureFlags = require('ReactFeatureFlags');
2122
var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
2223
var ReactDOMFiberComponent = require('ReactDOMFiberComponent');
2324
var ReactDOMFrameScheduling = require('ReactDOMFrameScheduling');
@@ -27,6 +28,8 @@ var ReactFiberReconciler = require('ReactFiberReconciler');
2728
var ReactInputSelection = require('ReactInputSelection');
2829
var ReactInstanceMap = require('ReactInstanceMap');
2930
var ReactPortal = require('ReactPortal');
31+
var { isValidElement } = require('ReactElement');
32+
3033

3134
var findDOMNode = require('findDOMNode');
3235
var invariant = require('invariant');
@@ -49,6 +52,7 @@ if (__DEV__) {
4952
var { updatedAncestorInfo } = validateDOMNesting;
5053
}
5154

55+
5256
const DOCUMENT_NODE = 9;
5357

5458
ReactDOMInjection.inject();
@@ -352,6 +356,17 @@ var ReactDOM = {
352356

353357
render(element : ReactElement<any>, container : DOMContainerElement, callback: ?Function) {
354358
validateContainer(container);
359+
360+
if (ReactFeatureFlags.disableNewFiberFeatures) {
361+
// Top-level check occurs here instead of inside child reconciler because
362+
// because requirements vary between renderers. E.g. React Art
363+
// allows arrays.
364+
invariant(
365+
isValidElement(element),
366+
'render(): Invalid component element.'
367+
);
368+
}
369+
355370
return renderSubtreeIntoContainer(null, element, container, callback);
356371
},
357372

src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js

Lines changed: 47 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,17 @@ var ReactTestUtils = require('ReactTestUtils');
1818

1919
describe('ReactDOMFiber', () => {
2020
var container;
21+
var ReactFeatureFlags;
2122

2223
beforeEach(() => {
2324
container = document.createElement('div');
25+
ReactFeatureFlags = require('ReactFeatureFlags');
26+
ReactFeatureFlags.disableNewFiberFeatures = false;
27+
});
28+
29+
afterEach(() => {
30+
ReactFeatureFlags = require('ReactFeatureFlags');
31+
ReactFeatureFlags.disableNewFiberFeatures = true;
2432
});
2533

2634
it('should render strings as children', () => {
@@ -1085,40 +1093,49 @@ describe('ReactDOMFiber', () => {
10851093
container
10861094
);
10871095
});
1096+
}
1097+
});
10881098

1089-
describe('disableNewFiberFeatures', () => {
1090-
var ReactFeatureFlags = require('ReactFeatureFlags');
1099+
// disableNewFiberFeatures currently defaults to true in test
1100+
describe('disableNewFiberFeatures', () => {
1101+
var container;
1102+
var ReactFeatureFlags;
10911103

1092-
beforeEach(() => {
1093-
ReactFeatureFlags.disableNewFiberFeatures = true;
1094-
});
1104+
beforeEach(() => {
1105+
container = document.createElement('div');
1106+
ReactFeatureFlags = require('ReactFeatureFlags');
1107+
ReactFeatureFlags.disableNewFiberFeatures = true;
1108+
});
10951109

1096-
afterEach(() => {
1097-
ReactFeatureFlags.disableNewFiberFeatures = false;
1098-
});
1110+
afterEach(() => {
1111+
ReactFeatureFlags = require('ReactFeatureFlags');
1112+
ReactFeatureFlags.disableNewFiberFeatures = false;
1113+
});
10991114

1100-
it('throws if non-element passed to top-level render', () => {
1101-
// FIXME: These assertions pass individually, but they leave React in
1102-
// an inconsistent state. This suggests an error-handling bug. I'll fix
1103-
// this in a separate PR.
1104-
const message = 'render(): Invalid component element.';
1105-
expect(() => ReactDOM.render(null, container)).toThrow(message, container);
1106-
expect(() => ReactDOM.render(undefined, container)).toThrow(message, container);
1107-
expect(() => ReactDOM.render(false, container)).toThrow(message, container);
1108-
expect(() => ReactDOM.render('Hi', container)).toThrow(message, container);
1109-
expect(() => ReactDOM.render(999, container)).toThrow(message, container);
1110-
expect(() => ReactDOM.render([<div />], container)).toThrow(message, container);
1111-
});
1115+
it('throws if non-element passed to top-level render', () => {
1116+
const message = 'render(): Invalid component element.';
1117+
expect(() => ReactDOM.render(null, container)).toThrow(message, container);
1118+
expect(() => ReactDOM.render(undefined, container)).toThrow(message, container);
1119+
expect(() => ReactDOM.render(false, container)).toThrow(message, container);
1120+
expect(() => ReactDOM.render('Hi', container)).toThrow(message, container);
1121+
expect(() => ReactDOM.render(999, container)).toThrow(message, container);
1122+
expect(() => ReactDOM.render([<div />], container)).toThrow(message, container);
1123+
});
11121124

1113-
it('throws if something other than false, null, or an element is returned from render', () => {
1114-
function Render(props) {
1115-
return props.children;
1116-
}
1125+
it('throws if something other than false, null, or an element is returned from render', () => {
1126+
function Render(props) {
1127+
return props.children;
1128+
}
11171129

1118-
expect(() => ReactDOM.render(<Render>Hi</Render>, container)).toThrow(/You may have returned undefined/);
1119-
expect(() => ReactDOM.render(<Render>{999}</Render>, container)).toThrow(/You may have returned undefined/);
1120-
expect(() => ReactDOM.render(<Render>[<div />]</Render>, container)).toThrow(/You may have returned undefined/);
1121-
});
1122-
});
1123-
}
1130+
expect(() => ReactDOM.render(<Render>Hi</Render>, container)).toThrow(/You may have returned undefined/);
1131+
expect(() => ReactDOM.render(<Render>{999}</Render>, container)).toThrow(/You may have returned undefined/);
1132+
expect(() => ReactDOM.render(<Render>[<div />]</Render>, container)).toThrow(/You may have returned undefined/);
1133+
});
1134+
1135+
it('treats mocked render functions as if they return null', () => {
1136+
class Mocked extends React.Component {}
1137+
Mocked.prototype.render = jest.fn();
1138+
ReactDOM.render(<Mocked />, container);
1139+
expect(container.textContent).toEqual('');
1140+
});
11241141
});

src/renderers/shared/fiber/ReactChildFiber.js

Lines changed: 84 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ const {
6464
FunctionalComponent,
6565
ClassComponent,
6666
HostText,
67-
HostRoot,
6867
HostPortal,
6968
CoroutineComponent,
7069
YieldComponent,
@@ -75,7 +74,6 @@ const {
7574
NoEffect,
7675
Placement,
7776
Deletion,
78-
Err,
7977
} = ReactTypeOfSideEffect;
8078

8179
function coerceRef(current: ?Fiber, element: ReactElement) {
@@ -1104,10 +1102,31 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
11041102
// not as a fragment. Nested arrays on the other hand will be treated as
11051103
// fragment nodes. Recursion happens at the normal flow.
11061104

1107-
if (ReactFeatureFlags.disableNewFiberFeatures) {
1105+
const disableNewFiberFeatures = ReactFeatureFlags.disableNewFiberFeatures;
1106+
1107+
// Handle object types
1108+
if (typeof newChild === 'object' && newChild !== null) {
11081109
// Support only the subset of return types that Stack supports. Treat
11091110
// everything else as empty, but log a warning.
1110-
if (typeof newChild === 'object' && newChild !== null) {
1111+
if (disableNewFiberFeatures) {
1112+
switch (newChild.$$typeof) {
1113+
case REACT_ELEMENT_TYPE:
1114+
return placeSingleChild(reconcileSingleElement(
1115+
returnFiber,
1116+
currentFirstChild,
1117+
newChild,
1118+
priority
1119+
));
1120+
1121+
case REACT_PORTAL_TYPE:
1122+
return placeSingleChild(reconcileSinglePortal(
1123+
returnFiber,
1124+
currentFirstChild,
1125+
newChild,
1126+
priority
1127+
));
1128+
}
1129+
} else {
11111130
switch (newChild.$$typeof) {
11121131
case REACT_ELEMENT_TYPE:
11131132
return placeSingleChild(reconcileSingleElement(
@@ -1117,6 +1136,22 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
11171136
priority
11181137
));
11191138

1139+
case REACT_COROUTINE_TYPE:
1140+
return placeSingleChild(reconcileSingleCoroutine(
1141+
returnFiber,
1142+
currentFirstChild,
1143+
newChild,
1144+
priority
1145+
));
1146+
1147+
case REACT_YIELD_TYPE:
1148+
return placeSingleChild(reconcileSingleYield(
1149+
returnFiber,
1150+
currentFirstChild,
1151+
newChild,
1152+
priority
1153+
));
1154+
11201155
case REACT_PORTAL_TYPE:
11211156
return placeSingleChild(reconcileSinglePortal(
11221157
returnFiber,
@@ -1126,44 +1161,37 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
11261161
));
11271162
}
11281163
}
1164+
}
11291165

1130-
if (returnFiber.tag === HostRoot) {
1131-
// Top-level only accepts elements or portals
1132-
invariant(
1133-
// If the root has an error effect, this is an intentional unmount.
1134-
// Don't throw an error.
1135-
returnFiber.effectTag & Err,
1136-
'render(): Invalid component element.'
1137-
);
1138-
} else {
1139-
switch (returnFiber.tag) {
1140-
case ClassComponent: {
1141-
if (__DEV__) {
1142-
const instance = returnFiber.stateNode;
1143-
if (instance.render._isMockFunction) {
1144-
// We allow auto-mocks to proceed as if they're
1145-
// returning null.
1146-
break;
1147-
}
1166+
if (disableNewFiberFeatures) {
1167+
// The new child is not an element. If it's not null or false,
1168+
// and the return fiber is a composite component, throw an error.
1169+
switch (returnFiber.tag) {
1170+
case ClassComponent: {
1171+
if (__DEV__) {
1172+
const instance = returnFiber.stateNode;
1173+
if (instance.render._isMockFunction && typeof newChild === 'undefined') {
1174+
// We allow auto-mocks to proceed as if they're
1175+
// returning null.
1176+
break;
11481177
}
11491178
}
1150-
// Intentionally fall through to the next case, which handles both
1151-
// functions and classes
1152-
// eslint-disable-next-lined no-fallthrough
1153-
case FunctionalComponent: {
1154-
// Composites accept elements, portals, null, or false
1155-
const Component = returnFiber.type;
1156-
invariant(
1157-
newChild === null || newChild === false,
1158-
'%s.render(): A valid React element (or null) must be ' +
1159-
'returned. You may have returned undefined, an array or some ' +
1160-
'other invalid object.',
1161-
Component.displayName || Component.name || 'Component'
1162-
);
1163-
}
1179+
}
1180+
// Intentionally fall through to the next case, which handles both
1181+
// functions and classes
1182+
// eslint-disable-next-lined no-fallthrough
1183+
case FunctionalComponent: {
1184+
// Composites accept elements, portals, null, or false
1185+
const Component = returnFiber.type;
1186+
invariant(
1187+
newChild === null || newChild === false,
1188+
'%s(...): A valid React element (or null) must be ' +
1189+
'returned. You may have returned undefined, an array or some ' +
1190+
'other invalid object.',
1191+
Component.displayName || Component.name || 'Component'
1192+
);
11641193
}
11651194
}
1166-
return deleteRemainingChildren(returnFiber, currentFirstChild);
11671195
}
11681196

11691197
if (typeof newChild === 'string' || typeof newChild === 'number') {
@@ -1175,65 +1203,29 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
11751203
));
11761204
}
11771205

1178-
if (typeof newChild === 'object' && newChild !== null) {
1179-
switch (newChild.$$typeof) {
1180-
case REACT_ELEMENT_TYPE:
1181-
return placeSingleChild(reconcileSingleElement(
1182-
returnFiber,
1183-
currentFirstChild,
1184-
newChild,
1185-
priority
1186-
));
1187-
1188-
case REACT_COROUTINE_TYPE:
1189-
return placeSingleChild(reconcileSingleCoroutine(
1190-
returnFiber,
1191-
currentFirstChild,
1192-
newChild,
1193-
priority
1194-
));
1195-
1196-
case REACT_YIELD_TYPE:
1197-
return placeSingleChild(reconcileSingleYield(
1198-
returnFiber,
1199-
currentFirstChild,
1200-
newChild,
1201-
priority
1202-
));
1203-
1204-
case REACT_PORTAL_TYPE:
1205-
return placeSingleChild(reconcileSinglePortal(
1206-
returnFiber,
1207-
currentFirstChild,
1208-
newChild,
1209-
priority
1210-
));
1211-
}
1212-
1213-
if (isArray(newChild)) {
1214-
return reconcileChildrenArray(
1215-
returnFiber,
1216-
currentFirstChild,
1217-
newChild,
1218-
priority
1219-
);
1220-
}
1206+
if (isArray(newChild)) {
1207+
return reconcileChildrenArray(
1208+
returnFiber,
1209+
currentFirstChild,
1210+
newChild,
1211+
priority
1212+
);
1213+
}
12211214

1222-
if (getIteratorFn(newChild)) {
1223-
return reconcileChildrenIterator(
1224-
returnFiber,
1225-
currentFirstChild,
1226-
newChild,
1227-
priority
1228-
);
1229-
}
1215+
if (getIteratorFn(newChild)) {
1216+
return reconcileChildrenIterator(
1217+
returnFiber,
1218+
currentFirstChild,
1219+
newChild,
1220+
priority
1221+
);
12301222
}
12311223

1232-
if (typeof newChild === 'undefined') {
1224+
if (!disableNewFiberFeatures && typeof newChild === 'undefined') {
1225+
// If the new child is undefined, and the return fiber is a composite
1226+
// component, throw an error. If Fiber return types are disabled,
1227+
// we already threw above.
12331228
switch (returnFiber.tag) {
1234-
case HostRoot:
1235-
// TODO: Top-level render
1236-
break;
12371229
case ClassComponent: {
12381230
if (__DEV__) {
12391231
const instance = returnFiber.stateNode;

0 commit comments

Comments
 (0)