@samitouri / QOS-React / commits / 70f704dca6

[Fiber] Nesting validation warnings (#8586)

* (WIP) Nesting warnings * Remove indirection * Add a note about namespace * Fix Flow and make host context required This makes it easier to avoid accidental nulls. I also added a test for the production case to avoid regressing because of __DEV__ branches.

Dan Abramov committed Dec 16, 2016 at 18:57 UTC 70f704dca6e5679dfda5867c44832a1ba5eb9b9e
15 files changed +420 -181
scripts/fiber/tests-passing-except-dev.txt
-4
@@ -3,10 +3,6 @@ src/addons/transitions/__tests__/ReactTransitionGroup-test.js
3
4 src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
5 * should not warn when server-side rendering `onScroll`
6 -* warns on invalid nesting
7 -* warns on invalid nesting at root
8 -* warns nicely for table rows
9 -* gives useful context in warnings
6 * should warn about incorrect casing on properties (ssr)
7 * should warn about incorrect casing on event handlers (ssr)
8 * should warn about class (ssr)
scripts/fiber/tests-passing.txt
+5
@@ -509,6 +509,7 @@ src/renderers/dom/__tests__/ReactDOMProduction-test.js
509 * should use prod React
510 * should handle a simple flow
511 * should call lifecycle methods
512 +* should keep track of namespace across portals in production
513
514 src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js
515 * should render strings as children
@@ -690,6 +691,10 @@ src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
691 * should throw when an attack vector is used server-side
692 * should throw when an invalid tag name is used
693 * should throw when an attack vector is used
694 +* warns on invalid nesting
695 +* warns on invalid nesting at root
696 +* warns nicely for table rows
697 +* gives useful context in warnings
698 * should warn about incorrect casing on properties
699 * should warn about incorrect casing on event handlers
700 * should warn about class
src/renderers/art/ReactARTFiber.js
+6 -1
@@ -18,6 +18,7 @@ require('art/modes/current').setCurrent(
18 const Mode = require('art/modes/current');
19 const Transform = require('art/core/transform');
20 const invariant = require('fbjs/lib/invariant');
21 +const emptyObject = require('emptyObject');
22 const React = require('React');
23 const ReactFiberReconciler = require('ReactFiberReconciler');
24
@@ -485,8 +486,12 @@ const ARTRenderer = ReactFiberReconciler({
486 // Noop
487 },
488
489 + getRootHostContext() {
490 + return emptyObject;
491 + },
492 +
493 getChildHostContext() {
489 - return null;
494 + return emptyObject;
495 },
496
497 scheduleAnimationCallback: window.requestAnimationFrame,
src/renderers/dom/__tests__/ReactDOMProduction-test.js
+59
@@ -13,6 +13,8 @@
13 describe('ReactDOMProduction', () => {
14 var oldProcess;
15
16 + var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
17 +
18 var React;
19 var ReactDOM;
20
@@ -193,4 +195,61 @@ describe('ReactDOMProduction', () => {
195 ' for full errors and additional helpful warnings.'
196 );
197 });
198 +
199 + if (ReactDOMFeatureFlags.useFiber) {
200 + // This test is originally from ReactDOMFiber-test but we replicate it here
201 + // to avoid production-only regressions because of host context differences
202 + // in dev and prod.
203 + it('should keep track of namespace across portals in production', () => {
204 + var svgEls, htmlEls;
205 + var expectSVG = {ref: el => svgEls.push(el)};
206 + var expectHTML = {ref: el => htmlEls.push(el)};
207 + var usePortal = function(tree) {
208 + return ReactDOM.unstable_createPortal(
209 + tree,
210 + document.createElement('div')
211 + );
212 + };
213 + var assertNamespacesMatch = function(tree) {
214 + var container = document.createElement('div');
215 + svgEls = [];
216 + htmlEls = [];
217 + ReactDOM.render(tree, container);
218 + svgEls.forEach(el => {
219 + expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg');
220 + });
221 + htmlEls.forEach(el => {
222 + expect(el.namespaceURI).toBe('http://www.w3.org/1999/xhtml');
223 + });
224 + ReactDOM.unmountComponentAtNode(container);
225 + expect(container.innerHTML).toBe('');
226 + };
227 +
228 + assertNamespacesMatch(
229 + <div {...expectHTML}>
230 + <svg {...expectSVG}>
231 + <foreignObject {...expectSVG}>
232 + <p {...expectHTML} />
233 + {usePortal(
234 + <svg {...expectSVG}>
235 + <image {...expectSVG} />
236 + <svg {...expectSVG}>
237 + <image {...expectSVG} />
238 + <foreignObject {...expectSVG}>
239 + <p {...expectHTML} />
240 + </foreignObject>
241 + {usePortal(<p {...expectHTML} />)}
242 + </svg>
243 + <image {...expectSVG} />
244 + </svg>
245 + )}
246 + <p {...expectHTML} />
247 + </foreignObject>
248 + <image {...expectSVG} />
249 + </svg>
250 + <p {...expectHTML} />
251 + </div>
252 + );
253 + });
254 + }
255 });
src/renderers/dom/fiber/ReactDOMFiber.js
+75 -7
@@ -39,6 +39,11 @@ var {
39 } = ReactDOMFiberComponent;
40 var { precacheFiberNode } = ReactDOMComponentTree;
41
42 +if (__DEV__) {
43 + var validateDOMNesting = require('validateDOMNesting');
44 + var { updatedAncestorInfo } = validateDOMNesting;
45 +}
46 +
47 const DOCUMENT_NODE = 9;
48
49 ReactDOMInjection.inject();
@@ -52,17 +57,44 @@ findDOMNode._injectFiber(function(fiber: Fiber) {
57 type DOMContainerElement = Element & { _reactRootContainer: ?Object };
58
59 type Container = Element;
55 -type Props = { className ?: string };
60 +type Props = { children ?: mixed };
61 type Instance = Element;
62 type TextInstance = Text;
63
64 +type HostContextDev = {
65 + namespace : string,
66 + ancestorInfo : mixed,
67 +};
68 +type HostContextProd = string;
69 +type HostContext = HostContextDev | HostContextProd;
70 +
71 let eventsEnabled : ?boolean = null;
72 let selectionInformation : ?mixed = null;
73
74 var DOMRenderer = ReactFiberReconciler({
75
64 - getChildHostContext(parentHostContext : string | null, type : string) {
65 - const parentNamespace = parentHostContext;
76 + getRootHostContext(rootContainerInstance : Container) : HostContext {
77 + const type = rootContainerInstance.tagName.toLowerCase();
78 + if (__DEV__) {
79 + const namespace = getChildNamespace(null, type);
80 + const isMountingIntoDocument = rootContainerInstance.ownerDocument.documentElement === rootContainerInstance;
81 + const ancestorInfo = updatedAncestorInfo(null, isMountingIntoDocument ? '#document' : type, null);
82 + return {namespace, ancestorInfo};
83 + }
84 + return getChildNamespace(null, type);
85 + },
86 +
87 + getChildHostContext(
88 + parentHostContext : HostContext,
89 + type : string,
90 + ) : HostContext {
91 + if (__DEV__) {
92 + const parentHostContextDev = ((parentHostContext : any) : HostContextDev);
93 + const namespace = getChildNamespace(parentHostContextDev.namespace, type);
94 + const ancestorInfo = updatedAncestorInfo(parentHostContextDev.ancestorInfo, type, null);
95 + return {namespace, ancestorInfo};
96 + }
97 + const parentNamespace = ((parentHostContext : any) : HostContextProd);
98 return getChildNamespace(parentNamespace, type);
99 },
100
@@ -83,10 +115,26 @@ var DOMRenderer = ReactFiberReconciler({
115 type : string,
116 props : Props,
117 rootContainerInstance : Container,
86 - hostContext : string | null,
118 + hostContext : HostContext,
119 internalInstanceHandle : Object,
120 ) : Instance {
89 - const domElement : Instance = createElement(type, props, rootContainerInstance, hostContext);
121 + let parentNamespace : string;
122 + if (__DEV__) {
123 + // TODO: take namespace into account when validating.
124 + const hostContextDev = ((hostContext : any) : HostContextDev);
125 + validateDOMNesting(type, null, null, hostContextDev.ancestorInfo);
126 + if (
127 + typeof props.children === 'string' ||
128 + typeof props.children === 'number'
129 + ) {
130 + const ownAncestorInfo = updatedAncestorInfo(hostContextDev.ancestorInfo, type, null);
131 + validateDOMNesting(null, String(props.children), null, ownAncestorInfo);
132 + }
133 + parentNamespace = hostContextDev.namespace;
134 + } else {
135 + parentNamespace = ((hostContext : any) : HostContextProd);
136 + }
137 + const domElement : Instance = createElement(type, props, rootContainerInstance, parentNamespace);
138 precacheFiberNode(internalInstanceHandle, domElement);
139 return domElement;
140 },
@@ -108,8 +156,19 @@ var DOMRenderer = ReactFiberReconciler({
156 domElement : Instance,
157 type : string,
158 oldProps : Props,
111 - newProps : Props
159 + newProps : Props,
160 + hostContext : HostContext,
161 ) : boolean {
162 + if (__DEV__) {
163 + const hostContextDev = ((hostContext : any) : HostContextDev);
164 + if (typeof newProps.children !== typeof oldProps.children && (
165 + typeof newProps.children === 'string' ||
166 + typeof newProps.children === 'number'
167 + )) {
168 + const ownAncestorInfo = updatedAncestorInfo(hostContextDev.ancestorInfo, type, null);
169 + validateDOMNesting(null, String(newProps.children), null, ownAncestorInfo);
170 + }
171 + }
172 return true;
173 },
174
@@ -143,7 +202,16 @@ var DOMRenderer = ReactFiberReconciler({
202 domElement.textContent = '';
203 },
204
146 - createTextInstance(text : string, rootContainerInstance : Container, internalInstanceHandle : Object) : TextInstance {
205 + createTextInstance(
206 + text : string,
207 + rootContainerInstance : Container,
208 + hostContext : HostContext,
209 + internalInstanceHandle : Object
210 + ) : TextInstance {
211 + if (__DEV__) {
212 + const hostContextDev = ((hostContext : any) : HostContextDev);
213 + validateDOMNesting(null, text, null, hostContextDev.ancestorInfo);
214 + }
215 var textNode : TextInstance = document.createTextNode(text);
216 precacheFiberNode(internalInstanceHandle, textNode);
217 return textNode;
src/renderers/dom/fiber/ReactDOMFiberComponent.js
+14 -10
@@ -56,6 +56,7 @@ var STYLE = 'style';
56 var HTML = '__html';
57
58 var {
59 + html: HTML_NAMESPACE,
60 svg: SVG_NAMESPACE,
61 mathml: MATH_NAMESPACE,
62 } = DOMNamespaces;
@@ -451,26 +452,26 @@ function updateDOMProperties(
452 }
453
454 // Assumes there is no parent namespace.
454 -function getIntrinsicNamespace(type : string) : string | null {
455 +function getIntrinsicNamespace(type : string) : string {
456 switch (type) {
457 case 'svg':
458 return SVG_NAMESPACE;
459 case 'math':
460 return MATH_NAMESPACE;
461 default:
461 - return null;
462 + return HTML_NAMESPACE;
463 }
464 }
465
466 var ReactDOMFiberComponent = {
466 - getChildNamespace(parentNamespace : string | null, type : string) : string | null {
467 - if (parentNamespace == null) {
468 - // No parent namespace: potential entry point.
467 + getChildNamespace(parentNamespace : string | null, type : string) : string {
468 + if (parentNamespace == null || parentNamespace === HTML_NAMESPACE) {
469 + // No (or default) parent namespace: potential entry point.
470 return getIntrinsicNamespace(type);
471 }
472 if (parentNamespace === SVG_NAMESPACE && type === 'foreignObject') {
473 // We're leaving SVG.
473 - return null;
474 + return HTML_NAMESPACE;
475 }
476 // By default, pass namespace below.
477 return parentNamespace;
@@ -480,14 +481,17 @@ var ReactDOMFiberComponent = {
481 type : string,
482 props : Object,
483 rootContainerElement : Element,
483 - parentNamespace : string | null
484 + parentNamespace : string
485 ) : Element {
486 // We create tags in the namespace of their parent container, except HTML
487 // tags get no namespace.
488 var ownerDocument = rootContainerElement.ownerDocument;
489 var domElement : Element;
489 - var namespaceURI = parentNamespace || getIntrinsicNamespace(type);
490 - if (namespaceURI == null) {
490 + var namespaceURI = parentNamespace;
491 + if (namespaceURI === HTML_NAMESPACE) {
492 + namespaceURI = getIntrinsicNamespace(type);
493 + }
494 + if (namespaceURI === HTML_NAMESPACE) {
495 if (__DEV__) {
496 warning(
497 type === type.toLowerCase() ||
@@ -649,7 +653,7 @@ var ReactDOMFiberComponent = {
653 tag : string,
654 lastRawProps : Object,
655 nextRawProps : Object,
652 - rootContainerElement : Element
656 + rootContainerElement : Element,
657 ) : void {
658 if (__DEV__) {
659 validatePropertiesInDevelopment(tag, nextRawProps);
src/renderers/dom/fiber/wrappers/ReactDOMFiberOption.js
+2 -7
@@ -15,25 +15,20 @@
15 var React = require('React');
16
17 var warning = require('warning');
18 -var didWarnInvalidOptionChildren = false;
18
19 function flattenChildren(children) {
20 var content = '';
21
22 // Flatten children and warn if they aren't strings or numbers;
23 // invalid types are ignored.
24 + // We can silently skip them because invalid DOM nesting warning
25 + // catches these cases in Fiber.
26 React.Children.forEach(children, function(child) {
27 if (child == null) {
28 return;
29 }
30 if (typeof child === 'string' || typeof child === 'number') {
31 content += child;
31 - } else if (!didWarnInvalidOptionChildren) {
32 - didWarnInvalidOptionChildren = true;
33 - warning(
34 - false,
35 - 'Only strings and numbers are supported as <option> children.'
36 - );
32 }
33 });
34
src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
+90 -25
@@ -1272,10 +1272,15 @@ describe('ReactDOMComponent', () => {
1272 spyOn(console, 'error');
1273 ReactTestUtils.renderIntoDocument(<div><tr /><tr /></div>);
1274
1275 + var addendum = ReactDOMFeatureFlags.useFiber ?
1276 + '\n in tr (at **)' +
1277 + '\n in div (at **)' :
1278 + ' See div > tr.';
1279 +
1280 expectDev(console.error.calls.count()).toBe(1);
1276 - expectDev(console.error.calls.argsFor(0)[0]).toBe(
1281 + expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
1282 'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
1278 - '<div>. See div > tr.'
1283 + '<div>.' + addendum
1284 );
1285 });
1286
@@ -1284,10 +1289,16 @@ describe('ReactDOMComponent', () => {
1289 var p = document.createElement('p');
1290 ReactDOM.render(<span><p /></span>, p);
1291
1292 + var addendum = ReactDOMFeatureFlags.useFiber ?
1293 + // There is no outer `p` here because root container is not part of the stack.
1294 + '\n in p (at **)' +
1295 + '\n in span (at **)' :
1296 + ' See p > ... > p.';
1297 +
1298 expectDev(console.error.calls.count()).toBe(1);
1288 - expectDev(console.error.calls.argsFor(0)[0]).toBe(
1299 + expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
1300 'Warning: validateDOMNesting(...): <p> cannot appear as a descendant ' +
1290 - 'of <p>. See p > ... > p.'
1301 + 'of <p>.' + addendum
1302 );
1303 });
1304
@@ -1307,22 +1318,39 @@ describe('ReactDOMComponent', () => {
1318 }
1319
1320 ReactTestUtils.renderIntoDocument(<Foo />);
1310 -
1321 expectDev(console.error.calls.count()).toBe(3);
1312 - expectDev(console.error.calls.argsFor(0)[0]).toBe(
1322 +
1323 + var addendum1 = ReactDOMFeatureFlags.useFiber ?
1324 + '\n in tr (at **)' +
1325 + '\n in Row (at **)' +
1326 + '\n in table (at **)' +
1327 + '\n in Foo (at **)' :
1328 + ' See Foo > table > Row > tr.';
1329 + expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
1330 'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
1314 - '<table>. See Foo > table > Row > tr. Add a <tbody> to your code to ' +
1315 - 'match the DOM tree generated by the browser.'
1331 + '<table>. Add a <tbody> to your code to match the DOM tree generated ' +
1332 + 'by the browser.' + addendum1
1333 );
1317 - expectDev(console.error.calls.argsFor(1)[0]).toBe(
1334 +
1335 + var addendum2 = ReactDOMFeatureFlags.useFiber ?
1336 + '\n in tr (at **)' +
1337 + '\n in Row (at **)' +
1338 + '\n in table (at **)' +
1339 + '\n in Foo (at **)' :
1340 + ' See Row > tr > #text.';
1341 + expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(1)[0])).toBe(
1342 'Warning: validateDOMNesting(...): Text nodes cannot appear as a ' +
1319 - 'child of <tr>. See Row > tr > #text.'
1343 + 'child of <tr>.' + addendum2
1344 );
1321 - expectDev(console.error.calls.argsFor(2)[0]).toBe(
1345 +
1346 + var addendum3 = ReactDOMFeatureFlags.useFiber ?
1347 + '\n in table (at **)' +
1348 + '\n in Foo (at **)' :
1349 + ' See Foo > table > #text.';
1350 + expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(2)[0])).toBe(
1351 'Warning: validateDOMNesting(...): Whitespace text nodes cannot ' +
1352 'appear as a child of <table>. Make sure you don\'t have any extra ' +
1324 - 'whitespace between tags on each line of your source code. See Foo > ' +
1325 - 'table > #text.'
1353 + 'whitespace between tags on each line of your source code.' + addendum3
1354 );
1355 });
1356
@@ -1355,8 +1383,14 @@ describe('ReactDOMComponent', () => {
1383 });
1384 ReactTestUtils.renderIntoDocument(<App1 />);
1385 expectDev(console.error.calls.count()).toBe(1);
1358 - expectDev(console.error.calls.argsFor(0)[0]).toContain(
1359 - 'See Viz1 > table > FancyRow > Row > tr.'
1386 + expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toContain(
1387 + ReactDOMFeatureFlags.useFiber ?
1388 + '\n in tr (at **)' +
1389 + '\n in Row (at **)' +
1390 + '\n in FancyRow (at **)' +
1391 + '\n in table (at **)' +
1392 + '\n in Viz1 (at **)' :
1393 + 'See Viz1 > table > FancyRow > Row > tr.'
1394 );
1395
1396 var Viz2 = React.createClass({
@@ -1367,26 +1401,51 @@ describe('ReactDOMComponent', () => {
1401 });
1402 ReactTestUtils.renderIntoDocument(<App2 />);
1403 expectDev(console.error.calls.count()).toBe(2);
1370 - expectDev(console.error.calls.argsFor(1)[0]).toContain(
1371 - 'See Viz2 > FancyTable > Table > table > FancyRow > Row > tr.'
1404 + expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(1)[0])).toContain(
1405 + ReactDOMFeatureFlags.useFiber ?
1406 + '\n in tr (at **)' +
1407 + '\n in Row (at **)' +
1408 + '\n in FancyRow (at **)' +
1409 + '\n in table (at **)' +
1410 + '\n in Table (at **)' +
1411 + '\n in FancyTable (at **)' +
1412 + '\n in Viz2 (at **)' :
1413 + 'See Viz2 > FancyTable > Table > table > FancyRow > Row > tr.'
1414 );
1415
1416 ReactTestUtils.renderIntoDocument(<FancyTable><FancyRow /></FancyTable>);
1417 expectDev(console.error.calls.count()).toBe(3);
1376 - expectDev(console.error.calls.argsFor(2)[0]).toContain(
1377 - 'See FancyTable > Table > table > FancyRow > Row > tr.'
1418 + expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(2)[0])).toContain(
1419 + ReactDOMFeatureFlags.useFiber ?
1420 + '\n in tr (at **)' +
1421 + '\n in Row (at **)' +
1422 + '\n in FancyRow (at **)' +
1423 + '\n in table (at **)' +
1424 + '\n in Table (at **)' +
1425 + '\n in FancyTable (at **)' :
1426 + 'See FancyTable > Table > table > FancyRow > Row > tr.'
1427 );
1428
1429 ReactTestUtils.renderIntoDocument(<table><FancyRow /></table>);
1430 expectDev(console.error.calls.count()).toBe(4);
1382 - expectDev(console.error.calls.argsFor(3)[0]).toContain(
1383 - 'See table > FancyRow > Row > tr.'
1431 + expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(3)[0])).toContain(
1432 + ReactDOMFeatureFlags.useFiber ?
1433 + '\n in tr (at **)' +
1434 + '\n in Row (at **)' +
1435 + '\n in FancyRow (at **)' +
1436 + '\n in table (at **)' :
1437 + 'See table > FancyRow > Row > tr.'
1438 );
1439
1440 ReactTestUtils.renderIntoDocument(<FancyTable><tr /></FancyTable>);
1441 expectDev(console.error.calls.count()).toBe(5);
1388 - expectDev(console.error.calls.argsFor(4)[0]).toContain(
1389 - 'See FancyTable > Table > table > tr.'
1442 + expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(4)[0])).toContain(
1443 + ReactDOMFeatureFlags.useFiber ?
1444 + '\n in tr (at **)' +
1445 + '\n in table (at **)' +
1446 + '\n in Table (at **)' +
1447 + '\n in FancyTable (at **)' :
1448 + 'See FancyTable > Table > table > tr.'
1449 );
1450
1451 class Link extends React.Component {
@@ -1397,8 +1456,14 @@ describe('ReactDOMComponent', () => {
1456
1457 ReactTestUtils.renderIntoDocument(<Link><div><Link /></div></Link>);
1458 expectDev(console.error.calls.count()).toBe(6);
1400 - expectDev(console.error.calls.argsFor(5)[0]).toContain(
1401 - 'See Link > a > ... > Link > a.'
1459 + expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(5)[0])).toContain(
1460 + ReactDOMFeatureFlags.useFiber ?
1461 + '\n in a (at **)' +
1462 + '\n in Link (at **)' +
1463 + '\n in div (at **)' +
1464 + '\n in a (at **)' +
1465 + '\n in Link (at **)' :
1466 + 'See Link > a > ... > Link > a.'
1467 );
1468 });
1469
src/renderers/dom/shared/validateDOMNesting.js
+105 -87
@@ -18,6 +18,8 @@ var warning = require('warning');
18 var validateDOMNesting = emptyFunction;
19
20 if (__DEV__) {
21 + var { getCurrentFiberStackAddendum } = require('ReactDebugCurrentFiber');
22 +
23 // This validation code was written based on the HTML5 parsing spec:
24 // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-scope
25 //
@@ -316,6 +318,49 @@ if (__DEV__) {
318 return stack;
319 };
320
321 + var getOwnerInfo = function(childInstance, childTag, ancestorInstance, ancestorTag, isParent) {
322 + var childOwner = childInstance && childInstance._currentElement._owner;
323 + var ancestorOwner = ancestorInstance && ancestorInstance._currentElement._owner;
324 +
325 + var childOwners = findOwnerStack(childOwner);
326 + var ancestorOwners = findOwnerStack(ancestorOwner);
327 +
328 + var minStackLen = Math.min(childOwners.length, ancestorOwners.length);
329 + var i;
330 +
331 + var deepestCommon = -1;
332 + for (i = 0; i < minStackLen; i++) {
333 + if (childOwners[i] === ancestorOwners[i]) {
334 + deepestCommon = i;
335 + } else {
336 + break;
337 + }
338 + }
339 +
340 + var UNKNOWN = '(unknown)';
341 + var childOwnerNames = childOwners.slice(deepestCommon + 1).map(
342 + (inst) => getComponentName(inst) || UNKNOWN
343 + );
344 + var ancestorOwnerNames = ancestorOwners.slice(deepestCommon + 1).map(
345 + (inst) => getComponentName(inst) || UNKNOWN
346 + );
347 + var ownerInfo = [].concat(
348 + // If the parent and child instances have a common owner ancestor, start
349 + // with that -- otherwise we just start with the parent's owners.
350 + deepestCommon !== -1 ?
351 + getComponentName(childOwners[deepestCommon]) || UNKNOWN :
352 + [],
353 + ancestorOwnerNames,
354 + ancestorTag,
355 + // If we're warning about an invalid (non-parent) ancestry, add '...'
356 + isParent ? [] : ['...'],
357 + childOwnerNames,
358 + childTag
359 + ).join(' > ');
360 +
361 + return ownerInfo;
362 + };
363 +
364 var didWarn = {};
365
366 validateDOMNesting = function(
@@ -340,101 +385,74 @@ if (__DEV__) {
385 isTagValidWithParent(childTag, parentTag) ? null : parentInfo;
386 var invalidAncestor =
387 invalidParent ? null : findInvalidAncestorForTag(childTag, ancestorInfo);
343 - var problematic = invalidParent || invalidAncestor;
344 -
345 - if (problematic) {
346 - var ancestorTag = problematic.tag;
347 - var ancestorInstance = problematic.instance;
348 -
349 - var childOwner = childInstance && childInstance._currentElement._owner;
350 - var ancestorOwner =
351 - ancestorInstance && ancestorInstance._currentElement._owner;
388 + var invalidParentOrAncestor = invalidParent || invalidAncestor;
389 + if (!invalidParentOrAncestor) {
390 + return;
391 + }
392
353 - var childOwners = findOwnerStack(childOwner);
354 - var ancestorOwners = findOwnerStack(ancestorOwner);
393 + var ancestorInstance = invalidParentOrAncestor.instance;
394 + var ancestorTag = invalidParentOrAncestor.tag;
395 + var addendum;
396
356 - var minStackLen = Math.min(childOwners.length, ancestorOwners.length);
357 - var i;
397 + if (childInstance != null) {
398 + addendum = ' See ' + getOwnerInfo(
399 + childInstance,
400 + childTag,
401 + ancestorInstance,
402 + ancestorTag,
403 + Boolean(invalidParent)
404 + ) + '.';
405 + } else {
406 + addendum = getCurrentFiberStackAddendum();
407 + }
408
359 - var deepestCommon = -1;
360 - for (i = 0; i < minStackLen; i++) {
361 - if (childOwners[i] === ancestorOwners[i]) {
362 - deepestCommon = i;
363 - } else {
364 - break;
365 - }
366 - }
409 + var warnKey =
410 + !!invalidParent + '|' + childTag + '|' + ancestorTag + '|' + addendum;
411 + if (didWarn[warnKey]) {
412 + return;
413 + }
414 + didWarn[warnKey] = true;
415
368 - var UNKNOWN = '(unknown)';
369 - var childOwnerNames = childOwners.slice(deepestCommon + 1).map(
370 - (inst) => getComponentName(inst) || UNKNOWN
371 - );
372 - var ancestorOwnerNames = ancestorOwners.slice(deepestCommon + 1).map(
373 - (inst) => getComponentName(inst) || UNKNOWN
374 - );
375 - var ownerInfo = [].concat(
376 - // If the parent and child instances have a common owner ancestor, start
377 - // with that -- otherwise we just start with the parent's owners.
378 - deepestCommon !== -1 ?
379 - getComponentName(childOwners[deepestCommon]) || UNKNOWN :
380 - [],
381 - ancestorOwnerNames,
382 - ancestorTag,
383 - // If we're warning about an invalid (non-parent) ancestry, add '...'
384 - invalidAncestor ? ['...'] : [],
385 - childOwnerNames,
386 - childTag
387 - ).join(' > ');
388 -
389 - var warnKey =
390 - !!invalidParent + '|' + childTag + '|' + ancestorTag + '|' + ownerInfo;
391 - if (didWarn[warnKey]) {
392 - return;
393 - }
394 - didWarn[warnKey] = true;
395 -
396 - var tagDisplayName = childTag;
397 - var whitespaceInfo = '';
398 - if (childTag === '#text') {
399 - if (/\S/.test(childText)) {
400 - tagDisplayName = 'Text nodes';
401 - } else {
402 - tagDisplayName = 'Whitespace text nodes';
403 - whitespaceInfo =
404 - ' Make sure you don\'t have any extra whitespace between tags on ' +
405 - 'each line of your source code.';
406 - }
416 + var tagDisplayName = childTag;
417 + var whitespaceInfo = '';
418 + if (childTag === '#text') {
419 + if (/\S/.test(childText)) {
420 + tagDisplayName = 'Text nodes';
421 } else {
408 - tagDisplayName = '<' + childTag + '>';
422 + tagDisplayName = 'Whitespace text nodes';
423 + whitespaceInfo =
424 + ' Make sure you don\'t have any extra whitespace between tags on ' +
425 + 'each line of your source code.';
426 }
427 + } else {
428 + tagDisplayName = '<' + childTag + '>';
429 + }
430
411 - if (invalidParent) {
412 - var info = '';
413 - if (ancestorTag === 'table' && childTag === 'tr') {
414 - info +=
415 - ' Add a <tbody> to your code to match the DOM tree generated by ' +
416 - 'the browser.';
417 - }
418 - warning(
419 - false,
420 - 'validateDOMNesting(...): %s cannot appear as a child of <%s>.%s ' +
421 - 'See %s.%s',
422 - tagDisplayName,
423 - ancestorTag,
424 - whitespaceInfo,
425 - ownerInfo,
426 - info
427 - );
428 - } else {
429 - warning(
430 - false,
431 - 'validateDOMNesting(...): %s cannot appear as a descendant of ' +
432 - '<%s>. See %s.',
433 - tagDisplayName,
434 - ancestorTag,
435 - ownerInfo
436 - );
431 + if (invalidParent) {
432 + var info = '';
433 + if (ancestorTag === 'table' && childTag === 'tr') {
434 + info +=
435 + ' Add a <tbody> to your code to match the DOM tree generated by ' +
436 + 'the browser.';
437 }
438 + warning(
439 + false,
440 + 'validateDOMNesting(...): %s cannot appear as a child of <%s>.%s%s%s',
441 + tagDisplayName,
442 + ancestorTag,
443 + whitespaceInfo,
444 + info,
445 + addendum
446 + );
447 + } else {
448 + warning(
449 + false,
450 + 'validateDOMNesting(...): %s cannot appear as a descendant of ' +
451 + '<%s>.%s',
452 + tagDisplayName,
453 + ancestorTag,
454 + addendum
455 + );
456 }
457 };
458
src/renderers/dom/shared/wrappers/__tests__/ReactDOMOption-test.js
+15 -6
@@ -13,13 +13,19 @@
13
14
15 describe('ReactDOMOption', () => {
16 + function normalizeCodeLocInfo(str) {
17 + return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
18 + }
19 +
20 var React;
21 var ReactDOM;
22 + var ReactDOMFeatureFlags;
23 var ReactTestUtils;
24
25 beforeEach(() => {
26 React = require('React');
27 ReactDOM = require('ReactDOM');
28 + ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
29 ReactTestUtils = require('ReactTestUtils');
30 });
31
@@ -33,15 +39,18 @@ describe('ReactDOMOption', () => {
39
40 it('should ignore and warn invalid children types', () => {
41 spyOn(console, 'error');
36 - var stub = <option>{1} <div /> {2}</option>;
37 - stub = ReactTestUtils.renderIntoDocument(stub);
38 - var node = ReactDOM.findDOMNode(stub);
42 + var el = <option>{1} <div /> {2}</option>;
43 + var node = ReactTestUtils.renderIntoDocument(el);
44 expect(node.innerHTML).toBe('1 2');
40 - ReactTestUtils.renderIntoDocument(<option>{1} <div /> {2}</option>);
45 + ReactTestUtils.renderIntoDocument(el);
46 // only warn once
47 expectDev(console.error.calls.count()).toBe(1);
43 - expectDev(console.error.calls.argsFor(0)[0]).toContain(
44 - 'Only strings and numbers are supported as <option> children.'
48 + expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toContain(
49 + ReactDOMFeatureFlags.useFiber ?
50 + '<div> cannot appear as a child of <option>.\n' +
51 + ' in div (at **)\n' +
52 + ' in option (at **)' :
53 + 'Only strings and numbers are supported as <option> children.'
54 );
55 });
56
src/renderers/native/ReactNativeFiber.js
+9 -6
@@ -29,6 +29,7 @@ const ReactPortal = require('ReactPortal');
29 const UIManager = require('UIManager');
30
31 const deepFreezeAndThrowOnMutationInDev = require('deepFreezeAndThrowOnMutationInDev');
32 +const emptyObject = require('emptyObject');
33 const findNodeHandle = require('findNodeHandle');
34 const invariant = require('invariant');
35
@@ -138,7 +139,7 @@ const NativeRenderer = ReactFiberReconciler({
139 type : string,
140 props : Props,
141 rootContainerInstance : Container,
141 - hostContext : string | null,
142 + hostContext : Object,
143 internalInstanceHandle : Object
144 ) : Instance {
145 const tag = ReactNativeTagHandles.allocateTag();
@@ -174,6 +175,7 @@ const NativeRenderer = ReactFiberReconciler({
175 createTextInstance(
176 text : string,
177 rootContainerInstance : Container,
178 + hostContext : Object,
179 internalInstanceHandle : Object,
180 ) : TextInstance {
181 const tag = ReactNativeTagHandles.allocateTag();
@@ -210,11 +212,12 @@ const NativeRenderer = ReactFiberReconciler({
212 );
213 },
214
213 - getChildHostContext(
214 - parentHostContext : string | null,
215 - type : string
216 - ) {
217 - return parentHostContext;
215 + getRootHostContext() {
216 + return emptyObject;
217 + },
218 +
219 + getChildHostContext() {
220 + return emptyObject;
221 },
222
223 insertBefore(
src/renderers/noop/ReactNoop.js
+12 -2
@@ -27,6 +27,7 @@ var ReactInstanceMap = require('ReactInstanceMap');
27 var {
28 AnimationPriority,
29 } = require('ReactPriorityLevel');
30 +var emptyObject = require('emptyObject');
31
32 var scheduledAnimationCallback = null;
33 var scheduledDeferredCallback = null;
@@ -40,8 +41,12 @@ var instanceCounter = 0;
41
42 var NoopRenderer = ReactFiberReconciler({
43
44 + getRootHostContext() {
45 + return emptyObject;
46 + },
47 +
48 getChildHostContext() {
44 - return null;
49 + return emptyObject;
50 },
51
52 createInstance(type : string, props : Props) : Instance {
@@ -81,7 +86,12 @@ var NoopRenderer = ReactFiberReconciler({
86
87 resetTextContent(instance : Instance) : void {},
88
84 - createTextInstance(text : string, rootContainerInstance : Container, internalInstanceHandle : Object) : TextInstance {
89 + createTextInstance(
90 + text : string,
91 + rootContainerInstance : Container,
92 + hostContext : Object,
93 + internalInstanceHandle : Object
94 + ) : TextInstance {
95 var inst = { text : text, id: instanceCounter++ };
96 // Hide from unit tests
97 Object.defineProperty(inst, 'id', { value: inst.id, enumerable: false });
src/renderers/shared/fiber/ReactFiberCompleteWork.js
+4 -2
@@ -213,7 +213,8 @@ module.exports = function<T, P, I, TI, C, CX>(
213 newProps = workInProgress.memoizedProps || oldProps;
214 }
215 const instance : I = workInProgress.stateNode;
216 - if (prepareUpdate(instance, type, oldProps, newProps)) {
216 + const currentHostContext = getHostContext();
217 + if (prepareUpdate(instance, type, oldProps, newProps, currentHostContext)) {
218 // This returns true if there was something to update.
219 markUpdate(workInProgress);
220 }
@@ -278,7 +279,8 @@ module.exports = function<T, P, I, TI, C, CX>(
279 }
280 }
281 const rootContainerInstance = getRootHostContainer();
281 - const textInstance = createTextInstance(newText, rootContainerInstance, workInProgress);
282 + const currentHostContext = getHostContext();
283 + const textInstance = createTextInstance(newText, rootContainerInstance, currentHostContext, workInProgress);
284 workInProgress.stateNode = textInstance;
285 }
286 workInProgress.memoizedProps = newText;
src/renderers/shared/fiber/ReactFiberHostContext.js
+19 -20
@@ -17,7 +17,7 @@ import type { HostConfig } from 'ReactFiberReconciler';
17
18 export type HostContext<C, CX> = {
19 getRootHostContainer() : C,
20 - getHostContext() : CX | null,
20 + getHostContext() : CX,
21
22 pushHostContext(fiber : Fiber) : void,
23 popHostContext(fiber : Fiber) : void,
@@ -32,12 +32,13 @@ module.exports = function<T, P, I, TI, C, CX>(
32 ) : HostContext<C, CX> {
33 const {
34 getChildHostContext,
35 + getRootHostContext,
36 } = config;
37
38 // Context stack is reused across the subtrees.
39 // We use a null sentinel on the fiber stack to separate them.
39 - let contextFibers : Array<Fiber | null> | null = null;
40 - let contextValues : Array<CX | null> | null = null;
40 + let contextFibers : Array<Fiber | null> = [];
41 + let contextValues : Array<CX | null> = [];
42 let contextDepth : number = -1;
43 // Current context for fast access.
44 let currentContextValue : CX | null = null;
@@ -55,6 +56,7 @@ module.exports = function<T, P, I, TI, C, CX>(
56 }
57
58 function pushHostContainer(nextRootInstance : C) {
59 + const nextRootContext = getRootHostContext(nextRootInstance);
60 if (rootInstance == null) {
61 // We're entering a root.
62 rootInstance = nextRootInstance;
@@ -64,14 +66,12 @@ module.exports = function<T, P, I, TI, C, CX>(
66 portalDepth++;
67 portalStack[portalDepth] = rootInstance;
68 rootInstance = nextRootInstance;
67 - // Delimit subtree context with a sentinel so we know where to pop later.
68 - if (contextFibers != null && contextValues != null) {
69 - contextDepth++;
70 - contextFibers[contextDepth] = null;
71 - contextValues[contextDepth] = null;
72 - currentContextValue = null;
73 - }
69 }
70 + // Push the next root or portal context.
71 + contextDepth++;
72 + contextFibers[contextDepth] = null;
73 + contextValues[contextDepth] = nextRootContext;
74 + currentContextValue = nextRootContext;
75 }
76
77 function popHostContainer() {
@@ -89,7 +89,7 @@ module.exports = function<T, P, I, TI, C, CX>(
89 // If we pushed any context while in a portal, we need to roll it back.
90 if (contextDepth > -1) {
91 contextDepth--;
92 - if (contextDepth > -1 && contextValues != null) {
92 + if (contextDepth > -1) {
93 currentContextValue = contextValues[contextDepth];
94 } else {
95 currentContextValue = null;
@@ -98,21 +98,21 @@ module.exports = function<T, P, I, TI, C, CX>(
98 }
99 }
100
101 - function getHostContext() : CX | null {
101 + function getHostContext() : CX {
102 + if (currentContextValue == null) {
103 + throw new Error('Expected host context to exist.');
104 + }
105 return currentContextValue;
106 }
107
108 function pushHostContext(fiber : Fiber) : void {
106 - const nextContextValue = getChildHostContext(currentContextValue, fiber.type);
109 + if (currentContextValue == null) {
110 + throw new Error('Expected root host context to exist.');
111 + }
112 + const nextContextValue = getChildHostContext(currentContextValue, fiber.type, rootInstance);
113 if (currentContextValue === nextContextValue) {
114 return;
115 }
110 - if (contextFibers == null) {
111 - contextFibers = [];
112 - }
113 - if (contextValues == null) {
114 - contextValues = [];
115 - }
116 contextDepth++;
117 contextFibers[contextDepth] = fiber;
118 contextValues[contextDepth] = nextContextValue;
@@ -144,7 +144,6 @@ module.exports = function<T, P, I, TI, C, CX>(
144 // Reset portal stack pointer because we're starting from the very top.
145 portalDepth = -1;
146 // Reset current container state.
147 - // Don't reset arrays because we reuse them.
147 rootInstance = null;
148 contextDepth = -1;
149 currentContextValue = null;
src/renderers/shared/fiber/ReactFiberReconciler.js
+5 -4
@@ -40,19 +40,20 @@ type OpaqueNode = Fiber;
40
41 export type HostConfig<T, P, I, TI, C, CX> = {
42
43 - getChildHostContext(parentHostContext : CX | null, type : T) : CX,
43 + getRootHostContext(rootContainerInstance : C) : CX,
44 + getChildHostContext(parentHostContext : CX, type : T) : CX,
45
45 - createInstance(type : T, props : P, rootContainerInstance : C, hostContext : CX | null, internalInstanceHandle : OpaqueNode) : I,
46 + createInstance(type : T, props : P, rootContainerInstance : C, hostContext : CX, internalInstanceHandle : OpaqueNode) : I,
47 appendInitialChild(parentInstance : I, child : I | TI) : void,
48 finalizeInitialChildren(parentInstance : I, type : T, props : P, rootContainerInstance : C) : void,
49
49 - prepareUpdate(instance : I, type : T, oldProps : P, newProps : P) : boolean,
50 + prepareUpdate(instance : I, type : T, oldProps : P, newProps : P, hostContext : CX) : boolean,
51 commitUpdate(instance : I, type : T, oldProps : P, newProps : P, rootContainerInstance : C, internalInstanceHandle : OpaqueNode) : void,
52
53 shouldSetTextContent(props : P) : boolean,
54 resetTextContent(instance : I) : void,
55
55 - createTextInstance(text : string, rootContainerInstance : C, internalInstanceHandle : OpaqueNode) : TI,
56 + createTextInstance(text : string, rootContainerInstance : C, hostContext : CX, internalInstanceHandle : OpaqueNode) : TI,
57 commitTextUpdate(textInstance : TI, oldText : string, newText : string) : void,
58
59 appendChild(parentInstance : I | C, child : I | TI) : void,