Skip to content

Commit 70f704d

Browse files
authored
[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.
1 parent e1eccbf commit 70f704d

15 files changed

+420
-181
lines changed

scripts/fiber/tests-passing-except-dev.txt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,6 @@ src/addons/transitions/__tests__/ReactTransitionGroup-test.js
33

44
src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
55
* 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
106
* should warn about incorrect casing on properties (ssr)
117
* should warn about incorrect casing on event handlers (ssr)
128
* should warn about class (ssr)

scripts/fiber/tests-passing.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,7 @@ src/renderers/dom/__tests__/ReactDOMProduction-test.js
509509
* should use prod React
510510
* should handle a simple flow
511511
* should call lifecycle methods
512+
* should keep track of namespace across portals in production
512513

513514
src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js
514515
* should render strings as children
@@ -690,6 +691,10 @@ src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
690691
* should throw when an attack vector is used server-side
691692
* should throw when an invalid tag name is used
692693
* 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
693698
* should warn about incorrect casing on properties
694699
* should warn about incorrect casing on event handlers
695700
* should warn about class

src/renderers/art/ReactARTFiber.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ require('art/modes/current').setCurrent(
1818
const Mode = require('art/modes/current');
1919
const Transform = require('art/core/transform');
2020
const invariant = require('fbjs/lib/invariant');
21+
const emptyObject = require('emptyObject');
2122
const React = require('React');
2223
const ReactFiberReconciler = require('ReactFiberReconciler');
2324

@@ -485,8 +486,12 @@ const ARTRenderer = ReactFiberReconciler({
485486
// Noop
486487
},
487488

489+
getRootHostContext() {
490+
return emptyObject;
491+
},
492+
488493
getChildHostContext() {
489-
return null;
494+
return emptyObject;
490495
},
491496

492497
scheduleAnimationCallback: window.requestAnimationFrame,

src/renderers/dom/__tests__/ReactDOMProduction-test.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
describe('ReactDOMProduction', () => {
1414
var oldProcess;
1515

16+
var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
17+
1618
var React;
1719
var ReactDOM;
1820

@@ -193,4 +195,61 @@ describe('ReactDOMProduction', () => {
193195
' for full errors and additional helpful warnings.'
194196
);
195197
});
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+
}
196255
});

src/renderers/dom/fiber/ReactDOMFiber.js

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ var {
3939
} = ReactDOMFiberComponent;
4040
var { precacheFiberNode } = ReactDOMComponentTree;
4141

42+
if (__DEV__) {
43+
var validateDOMNesting = require('validateDOMNesting');
44+
var { updatedAncestorInfo } = validateDOMNesting;
45+
}
46+
4247
const DOCUMENT_NODE = 9;
4348

4449
ReactDOMInjection.inject();
@@ -52,17 +57,44 @@ findDOMNode._injectFiber(function(fiber: Fiber) {
5257
type DOMContainerElement = Element & { _reactRootContainer: ?Object };
5358

5459
type Container = Element;
55-
type Props = { className ?: string };
60+
type Props = { children ?: mixed };
5661
type Instance = Element;
5762
type TextInstance = Text;
5863

64+
type HostContextDev = {
65+
namespace : string,
66+
ancestorInfo : mixed,
67+
};
68+
type HostContextProd = string;
69+
type HostContext = HostContextDev | HostContextProd;
70+
5971
let eventsEnabled : ?boolean = null;
6072
let selectionInformation : ?mixed = null;
6173

6274
var DOMRenderer = ReactFiberReconciler({
6375

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);
6698
return getChildNamespace(parentNamespace, type);
6799
},
68100

@@ -83,10 +115,26 @@ var DOMRenderer = ReactFiberReconciler({
83115
type : string,
84116
props : Props,
85117
rootContainerInstance : Container,
86-
hostContext : string | null,
118+
hostContext : HostContext,
87119
internalInstanceHandle : Object,
88120
) : 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);
90138
precacheFiberNode(internalInstanceHandle, domElement);
91139
return domElement;
92140
},
@@ -108,8 +156,19 @@ var DOMRenderer = ReactFiberReconciler({
108156
domElement : Instance,
109157
type : string,
110158
oldProps : Props,
111-
newProps : Props
159+
newProps : Props,
160+
hostContext : HostContext,
112161
) : 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+
}
113172
return true;
114173
},
115174

@@ -143,7 +202,16 @@ var DOMRenderer = ReactFiberReconciler({
143202
domElement.textContent = '';
144203
},
145204

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+
}
147215
var textNode : TextInstance = document.createTextNode(text);
148216
precacheFiberNode(internalInstanceHandle, textNode);
149217
return textNode;

src/renderers/dom/fiber/ReactDOMFiberComponent.js

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ var STYLE = 'style';
5656
var HTML = '__html';
5757

5858
var {
59+
html: HTML_NAMESPACE,
5960
svg: SVG_NAMESPACE,
6061
mathml: MATH_NAMESPACE,
6162
} = DOMNamespaces;
@@ -451,26 +452,26 @@ function updateDOMProperties(
451452
}
452453

453454
// Assumes there is no parent namespace.
454-
function getIntrinsicNamespace(type : string) : string | null {
455+
function getIntrinsicNamespace(type : string) : string {
455456
switch (type) {
456457
case 'svg':
457458
return SVG_NAMESPACE;
458459
case 'math':
459460
return MATH_NAMESPACE;
460461
default:
461-
return null;
462+
return HTML_NAMESPACE;
462463
}
463464
}
464465

465466
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.
469470
return getIntrinsicNamespace(type);
470471
}
471472
if (parentNamespace === SVG_NAMESPACE && type === 'foreignObject') {
472473
// We're leaving SVG.
473-
return null;
474+
return HTML_NAMESPACE;
474475
}
475476
// By default, pass namespace below.
476477
return parentNamespace;
@@ -480,14 +481,17 @@ var ReactDOMFiberComponent = {
480481
type : string,
481482
props : Object,
482483
rootContainerElement : Element,
483-
parentNamespace : string | null
484+
parentNamespace : string
484485
) : Element {
485486
// We create tags in the namespace of their parent container, except HTML
486487
// tags get no namespace.
487488
var ownerDocument = rootContainerElement.ownerDocument;
488489
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) {
491495
if (__DEV__) {
492496
warning(
493497
type === type.toLowerCase() ||
@@ -649,7 +653,7 @@ var ReactDOMFiberComponent = {
649653
tag : string,
650654
lastRawProps : Object,
651655
nextRawProps : Object,
652-
rootContainerElement : Element
656+
rootContainerElement : Element,
653657
) : void {
654658
if (__DEV__) {
655659
validatePropertiesInDevelopment(tag, nextRawProps);

src/renderers/dom/fiber/wrappers/ReactDOMFiberOption.js

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,20 @@
1515
var React = require('React');
1616

1717
var warning = require('warning');
18-
var didWarnInvalidOptionChildren = false;
1918

2019
function flattenChildren(children) {
2120
var content = '';
2221

2322
// Flatten children and warn if they aren't strings or numbers;
2423
// invalid types are ignored.
24+
// We can silently skip them because invalid DOM nesting warning
25+
// catches these cases in Fiber.
2526
React.Children.forEach(children, function(child) {
2627
if (child == null) {
2728
return;
2829
}
2930
if (typeof child === 'string' || typeof child === 'number') {
3031
content += child;
31-
} else if (!didWarnInvalidOptionChildren) {
32-
didWarnInvalidOptionChildren = true;
33-
warning(
34-
false,
35-
'Only strings and numbers are supported as <option> children.'
36-
);
3732
}
3833
});
3934

0 commit comments

Comments
 (0)