[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,