[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.
This commit is contained in:
Dan Abramov
2016-12-16 18:57:58 -08:00
committed by GitHub
parent e1eccbfbb0
commit 70f704dca6
15 changed files with 419 additions and 180 deletions
@@ -3,10 +3,6 @@ src/addons/transitions/__tests__/ReactTransitionGroup-test.js
src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
* should not warn when server-side rendering `onScroll`
* warns on invalid nesting
* warns on invalid nesting at root
* warns nicely for table rows
* gives useful context in warnings
* should warn about incorrect casing on properties (ssr)
* should warn about incorrect casing on event handlers (ssr)
* should warn about class (ssr)
+5
View File
@@ -509,6 +509,7 @@ src/renderers/dom/__tests__/ReactDOMProduction-test.js
* should use prod React
* should handle a simple flow
* should call lifecycle methods
* should keep track of namespace across portals in production
src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js
* should render strings as children
@@ -690,6 +691,10 @@ src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
* should throw when an attack vector is used server-side
* should throw when an invalid tag name is used
* should throw when an attack vector is used
* warns on invalid nesting
* warns on invalid nesting at root
* warns nicely for table rows
* gives useful context in warnings
* should warn about incorrect casing on properties
* should warn about incorrect casing on event handlers
* should warn about class
+6 -1
View File
@@ -18,6 +18,7 @@ require('art/modes/current').setCurrent(
const Mode = require('art/modes/current');
const Transform = require('art/core/transform');
const invariant = require('fbjs/lib/invariant');
const emptyObject = require('emptyObject');
const React = require('React');
const ReactFiberReconciler = require('ReactFiberReconciler');
@@ -485,8 +486,12 @@ const ARTRenderer = ReactFiberReconciler({
// Noop
},
getRootHostContext() {
return emptyObject;
},
getChildHostContext() {
return null;
return emptyObject;
},
scheduleAnimationCallback: window.requestAnimationFrame,
@@ -13,6 +13,8 @@
describe('ReactDOMProduction', () => {
var oldProcess;
var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
var React;
var ReactDOM;
@@ -193,4 +195,61 @@ describe('ReactDOMProduction', () => {
' for full errors and additional helpful warnings.'
);
});
if (ReactDOMFeatureFlags.useFiber) {
// This test is originally from ReactDOMFiber-test but we replicate it here
// to avoid production-only regressions because of host context differences
// in dev and prod.
it('should keep track of namespace across portals in production', () => {
var svgEls, htmlEls;
var expectSVG = {ref: el => svgEls.push(el)};
var expectHTML = {ref: el => htmlEls.push(el)};
var usePortal = function(tree) {
return ReactDOM.unstable_createPortal(
tree,
document.createElement('div')
);
};
var assertNamespacesMatch = function(tree) {
var container = document.createElement('div');
svgEls = [];
htmlEls = [];
ReactDOM.render(tree, container);
svgEls.forEach(el => {
expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg');
});
htmlEls.forEach(el => {
expect(el.namespaceURI).toBe('http://www.w3.org/1999/xhtml');
});
ReactDOM.unmountComponentAtNode(container);
expect(container.innerHTML).toBe('');
};
assertNamespacesMatch(
<div {...expectHTML}>
<svg {...expectSVG}>
<foreignObject {...expectSVG}>
<p {...expectHTML} />
{usePortal(
<svg {...expectSVG}>
<image {...expectSVG} />
<svg {...expectSVG}>
<image {...expectSVG} />
<foreignObject {...expectSVG}>
<p {...expectHTML} />
</foreignObject>
{usePortal(<p {...expectHTML} />)}
</svg>
<image {...expectSVG} />
</svg>
)}
<p {...expectHTML} />
</foreignObject>
<image {...expectSVG} />
</svg>
<p {...expectHTML} />
</div>
);
});
}
});
+75 -7
View File
@@ -39,6 +39,11 @@ var {
} = ReactDOMFiberComponent;
var { precacheFiberNode } = ReactDOMComponentTree;
if (__DEV__) {
var validateDOMNesting = require('validateDOMNesting');
var { updatedAncestorInfo } = validateDOMNesting;
}
const DOCUMENT_NODE = 9;
ReactDOMInjection.inject();
@@ -52,17 +57,44 @@ findDOMNode._injectFiber(function(fiber: Fiber) {
type DOMContainerElement = Element & { _reactRootContainer: ?Object };
type Container = Element;
type Props = { className ?: string };
type Props = { children ?: mixed };
type Instance = Element;
type TextInstance = Text;
type HostContextDev = {
namespace : string,
ancestorInfo : mixed,
};
type HostContextProd = string;
type HostContext = HostContextDev | HostContextProd;
let eventsEnabled : ?boolean = null;
let selectionInformation : ?mixed = null;
var DOMRenderer = ReactFiberReconciler({
getChildHostContext(parentHostContext : string | null, type : string) {
const parentNamespace = parentHostContext;
getRootHostContext(rootContainerInstance : Container) : HostContext {
const type = rootContainerInstance.tagName.toLowerCase();
if (__DEV__) {
const namespace = getChildNamespace(null, type);
const isMountingIntoDocument = rootContainerInstance.ownerDocument.documentElement === rootContainerInstance;
const ancestorInfo = updatedAncestorInfo(null, isMountingIntoDocument ? '#document' : type, null);
return {namespace, ancestorInfo};
}
return getChildNamespace(null, type);
},
getChildHostContext(
parentHostContext : HostContext,
type : string,
) : HostContext {
if (__DEV__) {
const parentHostContextDev = ((parentHostContext : any) : HostContextDev);
const namespace = getChildNamespace(parentHostContextDev.namespace, type);
const ancestorInfo = updatedAncestorInfo(parentHostContextDev.ancestorInfo, type, null);
return {namespace, ancestorInfo};
}
const parentNamespace = ((parentHostContext : any) : HostContextProd);
return getChildNamespace(parentNamespace, type);
},
@@ -83,10 +115,26 @@ var DOMRenderer = ReactFiberReconciler({
type : string,
props : Props,
rootContainerInstance : Container,
hostContext : string | null,
hostContext : HostContext,
internalInstanceHandle : Object,
) : Instance {
const domElement : Instance = createElement(type, props, rootContainerInstance, hostContext);
let parentNamespace : string;
if (__DEV__) {
// TODO: take namespace into account when validating.
const hostContextDev = ((hostContext : any) : HostContextDev);
validateDOMNesting(type, null, null, hostContextDev.ancestorInfo);
if (
typeof props.children === 'string' ||
typeof props.children === 'number'
) {
const ownAncestorInfo = updatedAncestorInfo(hostContextDev.ancestorInfo, type, null);
validateDOMNesting(null, String(props.children), null, ownAncestorInfo);
}
parentNamespace = hostContextDev.namespace;
} else {
parentNamespace = ((hostContext : any) : HostContextProd);
}
const domElement : Instance = createElement(type, props, rootContainerInstance, parentNamespace);
precacheFiberNode(internalInstanceHandle, domElement);
return domElement;
},
@@ -108,8 +156,19 @@ var DOMRenderer = ReactFiberReconciler({
domElement : Instance,
type : string,
oldProps : Props,
newProps : Props
newProps : Props,
hostContext : HostContext,
) : boolean {
if (__DEV__) {
const hostContextDev = ((hostContext : any) : HostContextDev);
if (typeof newProps.children !== typeof oldProps.children && (
typeof newProps.children === 'string' ||
typeof newProps.children === 'number'
)) {
const ownAncestorInfo = updatedAncestorInfo(hostContextDev.ancestorInfo, type, null);
validateDOMNesting(null, String(newProps.children), null, ownAncestorInfo);
}
}
return true;
},
@@ -143,7 +202,16 @@ var DOMRenderer = ReactFiberReconciler({
domElement.textContent = '';
},
createTextInstance(text : string, rootContainerInstance : Container, internalInstanceHandle : Object) : TextInstance {
createTextInstance(
text : string,
rootContainerInstance : Container,
hostContext : HostContext,
internalInstanceHandle : Object
) : TextInstance {
if (__DEV__) {
const hostContextDev = ((hostContext : any) : HostContextDev);
validateDOMNesting(null, text, null, hostContextDev.ancestorInfo);
}
var textNode : TextInstance = document.createTextNode(text);
precacheFiberNode(internalInstanceHandle, textNode);
return textNode;
@@ -56,6 +56,7 @@ var STYLE = 'style';
var HTML = '__html';
var {
html: HTML_NAMESPACE,
svg: SVG_NAMESPACE,
mathml: MATH_NAMESPACE,
} = DOMNamespaces;
@@ -451,26 +452,26 @@ function updateDOMProperties(
}
// Assumes there is no parent namespace.
function getIntrinsicNamespace(type : string) : string | null {
function getIntrinsicNamespace(type : string) : string {
switch (type) {
case 'svg':
return SVG_NAMESPACE;
case 'math':
return MATH_NAMESPACE;
default:
return null;
return HTML_NAMESPACE;
}
}
var ReactDOMFiberComponent = {
getChildNamespace(parentNamespace : string | null, type : string) : string | null {
if (parentNamespace == null) {
// No parent namespace: potential entry point.
getChildNamespace(parentNamespace : string | null, type : string) : string {
if (parentNamespace == null || parentNamespace === HTML_NAMESPACE) {
// No (or default) parent namespace: potential entry point.
return getIntrinsicNamespace(type);
}
if (parentNamespace === SVG_NAMESPACE && type === 'foreignObject') {
// We're leaving SVG.
return null;
return HTML_NAMESPACE;
}
// By default, pass namespace below.
return parentNamespace;
@@ -480,14 +481,17 @@ var ReactDOMFiberComponent = {
type : string,
props : Object,
rootContainerElement : Element,
parentNamespace : string | null
parentNamespace : string
) : Element {
// We create tags in the namespace of their parent container, except HTML
// tags get no namespace.
var ownerDocument = rootContainerElement.ownerDocument;
var domElement : Element;
var namespaceURI = parentNamespace || getIntrinsicNamespace(type);
if (namespaceURI == null) {
var namespaceURI = parentNamespace;
if (namespaceURI === HTML_NAMESPACE) {
namespaceURI = getIntrinsicNamespace(type);
}
if (namespaceURI === HTML_NAMESPACE) {
if (__DEV__) {
warning(
type === type.toLowerCase() ||
@@ -649,7 +653,7 @@ var ReactDOMFiberComponent = {
tag : string,
lastRawProps : Object,
nextRawProps : Object,
rootContainerElement : Element
rootContainerElement : Element,
) : void {
if (__DEV__) {
validatePropertiesInDevelopment(tag, nextRawProps);
@@ -15,25 +15,20 @@
var React = require('React');
var warning = require('warning');
var didWarnInvalidOptionChildren = false;
function flattenChildren(children) {
var content = '';
// Flatten children and warn if they aren't strings or numbers;
// invalid types are ignored.
// We can silently skip them because invalid DOM nesting warning
// catches these cases in Fiber.
React.Children.forEach(children, function(child) {
if (child == null) {
return;
}
if (typeof child === 'string' || typeof child === 'number') {
content += child;
} else if (!didWarnInvalidOptionChildren) {
didWarnInvalidOptionChildren = true;
warning(
false,
'Only strings and numbers are supported as <option> children.'
);
}
});
@@ -1272,10 +1272,15 @@ describe('ReactDOMComponent', () => {
spyOn(console, 'error');
ReactTestUtils.renderIntoDocument(<div><tr /><tr /></div>);
var addendum = ReactDOMFeatureFlags.useFiber ?
'\n in tr (at **)' +
'\n in div (at **)' :
' See div > tr.';
expectDev(console.error.calls.count()).toBe(1);
expectDev(console.error.calls.argsFor(0)[0]).toBe(
expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
'<div>. See div > tr.'
'<div>.' + addendum
);
});
@@ -1284,10 +1289,16 @@ describe('ReactDOMComponent', () => {
var p = document.createElement('p');
ReactDOM.render(<span><p /></span>, p);
var addendum = ReactDOMFeatureFlags.useFiber ?
// There is no outer `p` here because root container is not part of the stack.
'\n in p (at **)' +
'\n in span (at **)' :
' See p > ... > p.';
expectDev(console.error.calls.count()).toBe(1);
expectDev(console.error.calls.argsFor(0)[0]).toBe(
expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
'Warning: validateDOMNesting(...): <p> cannot appear as a descendant ' +
'of <p>. See p > ... > p.'
'of <p>.' + addendum
);
});
@@ -1307,22 +1318,39 @@ describe('ReactDOMComponent', () => {
}
ReactTestUtils.renderIntoDocument(<Foo />);
expectDev(console.error.calls.count()).toBe(3);
expectDev(console.error.calls.argsFor(0)[0]).toBe(
var addendum1 = ReactDOMFeatureFlags.useFiber ?
'\n in tr (at **)' +
'\n in Row (at **)' +
'\n in table (at **)' +
'\n in Foo (at **)' :
' See Foo > table > Row > tr.';
expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
'<table>. See Foo > table > Row > tr. Add a <tbody> to your code to ' +
'match the DOM tree generated by the browser.'
'<table>. Add a <tbody> to your code to match the DOM tree generated ' +
'by the browser.' + addendum1
);
expectDev(console.error.calls.argsFor(1)[0]).toBe(
var addendum2 = ReactDOMFeatureFlags.useFiber ?
'\n in tr (at **)' +
'\n in Row (at **)' +
'\n in table (at **)' +
'\n in Foo (at **)' :
' See Row > tr > #text.';
expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(1)[0])).toBe(
'Warning: validateDOMNesting(...): Text nodes cannot appear as a ' +
'child of <tr>. See Row > tr > #text.'
'child of <tr>.' + addendum2
);
expectDev(console.error.calls.argsFor(2)[0]).toBe(
var addendum3 = ReactDOMFeatureFlags.useFiber ?
'\n in table (at **)' +
'\n in Foo (at **)' :
' See Foo > table > #text.';
expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(2)[0])).toBe(
'Warning: validateDOMNesting(...): Whitespace text nodes cannot ' +
'appear as a child of <table>. Make sure you don\'t have any extra ' +
'whitespace between tags on each line of your source code. See Foo > ' +
'table > #text.'
'whitespace between tags on each line of your source code.' + addendum3
);
});
@@ -1355,8 +1383,14 @@ describe('ReactDOMComponent', () => {
});
ReactTestUtils.renderIntoDocument(<App1 />);
expectDev(console.error.calls.count()).toBe(1);
expectDev(console.error.calls.argsFor(0)[0]).toContain(
'See Viz1 > table > FancyRow > Row > tr.'
expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toContain(
ReactDOMFeatureFlags.useFiber ?
'\n in tr (at **)' +
'\n in Row (at **)' +
'\n in FancyRow (at **)' +
'\n in table (at **)' +
'\n in Viz1 (at **)' :
'See Viz1 > table > FancyRow > Row > tr.'
);
var Viz2 = React.createClass({
@@ -1367,26 +1401,51 @@ describe('ReactDOMComponent', () => {
});
ReactTestUtils.renderIntoDocument(<App2 />);
expectDev(console.error.calls.count()).toBe(2);
expectDev(console.error.calls.argsFor(1)[0]).toContain(
'See Viz2 > FancyTable > Table > table > FancyRow > Row > tr.'
expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(1)[0])).toContain(
ReactDOMFeatureFlags.useFiber ?
'\n in tr (at **)' +
'\n in Row (at **)' +
'\n in FancyRow (at **)' +
'\n in table (at **)' +
'\n in Table (at **)' +
'\n in FancyTable (at **)' +
'\n in Viz2 (at **)' :
'See Viz2 > FancyTable > Table > table > FancyRow > Row > tr.'
);
ReactTestUtils.renderIntoDocument(<FancyTable><FancyRow /></FancyTable>);
expectDev(console.error.calls.count()).toBe(3);
expectDev(console.error.calls.argsFor(2)[0]).toContain(
'See FancyTable > Table > table > FancyRow > Row > tr.'
expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(2)[0])).toContain(
ReactDOMFeatureFlags.useFiber ?
'\n in tr (at **)' +
'\n in Row (at **)' +
'\n in FancyRow (at **)' +
'\n in table (at **)' +
'\n in Table (at **)' +
'\n in FancyTable (at **)' :
'See FancyTable > Table > table > FancyRow > Row > tr.'
);
ReactTestUtils.renderIntoDocument(<table><FancyRow /></table>);
expectDev(console.error.calls.count()).toBe(4);
expectDev(console.error.calls.argsFor(3)[0]).toContain(
'See table > FancyRow > Row > tr.'
expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(3)[0])).toContain(
ReactDOMFeatureFlags.useFiber ?
'\n in tr (at **)' +
'\n in Row (at **)' +
'\n in FancyRow (at **)' +
'\n in table (at **)' :
'See table > FancyRow > Row > tr.'
);
ReactTestUtils.renderIntoDocument(<FancyTable><tr /></FancyTable>);
expectDev(console.error.calls.count()).toBe(5);
expectDev(console.error.calls.argsFor(4)[0]).toContain(
'See FancyTable > Table > table > tr.'
expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(4)[0])).toContain(
ReactDOMFeatureFlags.useFiber ?
'\n in tr (at **)' +
'\n in table (at **)' +
'\n in Table (at **)' +
'\n in FancyTable (at **)' :
'See FancyTable > Table > table > tr.'
);
class Link extends React.Component {
@@ -1397,8 +1456,14 @@ describe('ReactDOMComponent', () => {
ReactTestUtils.renderIntoDocument(<Link><div><Link /></div></Link>);
expectDev(console.error.calls.count()).toBe(6);
expectDev(console.error.calls.argsFor(5)[0]).toContain(
'See Link > a > ... > Link > a.'
expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(5)[0])).toContain(
ReactDOMFeatureFlags.useFiber ?
'\n in a (at **)' +
'\n in Link (at **)' +
'\n in div (at **)' +
'\n in a (at **)' +
'\n in Link (at **)' :
'See Link > a > ... > Link > a.'
);
});
+104 -86
View File
@@ -18,6 +18,8 @@ var warning = require('warning');
var validateDOMNesting = emptyFunction;
if (__DEV__) {
var { getCurrentFiberStackAddendum } = require('ReactDebugCurrentFiber');
// This validation code was written based on the HTML5 parsing spec:
// https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-scope
//
@@ -316,6 +318,49 @@ if (__DEV__) {
return stack;
};
var getOwnerInfo = function(childInstance, childTag, ancestorInstance, ancestorTag, isParent) {
var childOwner = childInstance && childInstance._currentElement._owner;
var ancestorOwner = ancestorInstance && ancestorInstance._currentElement._owner;
var childOwners = findOwnerStack(childOwner);
var ancestorOwners = findOwnerStack(ancestorOwner);
var minStackLen = Math.min(childOwners.length, ancestorOwners.length);
var i;
var deepestCommon = -1;
for (i = 0; i < minStackLen; i++) {
if (childOwners[i] === ancestorOwners[i]) {
deepestCommon = i;
} else {
break;
}
}
var UNKNOWN = '(unknown)';
var childOwnerNames = childOwners.slice(deepestCommon + 1).map(
(inst) => getComponentName(inst) || UNKNOWN
);
var ancestorOwnerNames = ancestorOwners.slice(deepestCommon + 1).map(
(inst) => getComponentName(inst) || UNKNOWN
);
var ownerInfo = [].concat(
// If the parent and child instances have a common owner ancestor, start
// with that -- otherwise we just start with the parent's owners.
deepestCommon !== -1 ?
getComponentName(childOwners[deepestCommon]) || UNKNOWN :
[],
ancestorOwnerNames,
ancestorTag,
// If we're warning about an invalid (non-parent) ancestry, add '...'
isParent ? [] : ['...'],
childOwnerNames,
childTag
).join(' > ');
return ownerInfo;
};
var didWarn = {};
validateDOMNesting = function(
@@ -340,101 +385,74 @@ if (__DEV__) {
isTagValidWithParent(childTag, parentTag) ? null : parentInfo;
var invalidAncestor =
invalidParent ? null : findInvalidAncestorForTag(childTag, ancestorInfo);
var problematic = invalidParent || invalidAncestor;
var invalidParentOrAncestor = invalidParent || invalidAncestor;
if (!invalidParentOrAncestor) {
return;
}
if (problematic) {
var ancestorTag = problematic.tag;
var ancestorInstance = problematic.instance;
var ancestorInstance = invalidParentOrAncestor.instance;
var ancestorTag = invalidParentOrAncestor.tag;
var addendum;
var childOwner = childInstance && childInstance._currentElement._owner;
var ancestorOwner =
ancestorInstance && ancestorInstance._currentElement._owner;
var childOwners = findOwnerStack(childOwner);
var ancestorOwners = findOwnerStack(ancestorOwner);
var minStackLen = Math.min(childOwners.length, ancestorOwners.length);
var i;
var deepestCommon = -1;
for (i = 0; i < minStackLen; i++) {
if (childOwners[i] === ancestorOwners[i]) {
deepestCommon = i;
} else {
break;
}
}
var UNKNOWN = '(unknown)';
var childOwnerNames = childOwners.slice(deepestCommon + 1).map(
(inst) => getComponentName(inst) || UNKNOWN
);
var ancestorOwnerNames = ancestorOwners.slice(deepestCommon + 1).map(
(inst) => getComponentName(inst) || UNKNOWN
);
var ownerInfo = [].concat(
// If the parent and child instances have a common owner ancestor, start
// with that -- otherwise we just start with the parent's owners.
deepestCommon !== -1 ?
getComponentName(childOwners[deepestCommon]) || UNKNOWN :
[],
ancestorOwnerNames,
if (childInstance != null) {
addendum = ' See ' + getOwnerInfo(
childInstance,
childTag,
ancestorInstance,
ancestorTag,
// If we're warning about an invalid (non-parent) ancestry, add '...'
invalidAncestor ? ['...'] : [],
childOwnerNames,
childTag
).join(' > ');
Boolean(invalidParent)
) + '.';
} else {
addendum = getCurrentFiberStackAddendum();
}
var warnKey =
!!invalidParent + '|' + childTag + '|' + ancestorTag + '|' + ownerInfo;
if (didWarn[warnKey]) {
return;
}
didWarn[warnKey] = true;
var warnKey =
!!invalidParent + '|' + childTag + '|' + ancestorTag + '|' + addendum;
if (didWarn[warnKey]) {
return;
}
didWarn[warnKey] = true;
var tagDisplayName = childTag;
var whitespaceInfo = '';
if (childTag === '#text') {
if (/\S/.test(childText)) {
tagDisplayName = 'Text nodes';
} else {
tagDisplayName = 'Whitespace text nodes';
whitespaceInfo =
' Make sure you don\'t have any extra whitespace between tags on ' +
'each line of your source code.';
}
var tagDisplayName = childTag;
var whitespaceInfo = '';
if (childTag === '#text') {
if (/\S/.test(childText)) {
tagDisplayName = 'Text nodes';
} else {
tagDisplayName = '<' + childTag + '>';
tagDisplayName = 'Whitespace text nodes';
whitespaceInfo =
' Make sure you don\'t have any extra whitespace between tags on ' +
'each line of your source code.';
}
} else {
tagDisplayName = '<' + childTag + '>';
}
if (invalidParent) {
var info = '';
if (ancestorTag === 'table' && childTag === 'tr') {
info +=
' Add a <tbody> to your code to match the DOM tree generated by ' +
'the browser.';
}
warning(
false,
'validateDOMNesting(...): %s cannot appear as a child of <%s>.%s ' +
'See %s.%s',
tagDisplayName,
ancestorTag,
whitespaceInfo,
ownerInfo,
info
);
} else {
warning(
false,
'validateDOMNesting(...): %s cannot appear as a descendant of ' +
'<%s>. See %s.',
tagDisplayName,
ancestorTag,
ownerInfo
);
if (invalidParent) {
var info = '';
if (ancestorTag === 'table' && childTag === 'tr') {
info +=
' Add a <tbody> to your code to match the DOM tree generated by ' +
'the browser.';
}
warning(
false,
'validateDOMNesting(...): %s cannot appear as a child of <%s>.%s%s%s',
tagDisplayName,
ancestorTag,
whitespaceInfo,
info,
addendum
);
} else {
warning(
false,
'validateDOMNesting(...): %s cannot appear as a descendant of ' +
'<%s>.%s',
tagDisplayName,
ancestorTag,
addendum
);
}
};
@@ -13,13 +13,19 @@
describe('ReactDOMOption', () => {
function normalizeCodeLocInfo(str) {
return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
}
var React;
var ReactDOM;
var ReactDOMFeatureFlags;
var ReactTestUtils;
beforeEach(() => {
React = require('React');
ReactDOM = require('ReactDOM');
ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
ReactTestUtils = require('ReactTestUtils');
});
@@ -33,15 +39,18 @@ describe('ReactDOMOption', () => {
it('should ignore and warn invalid children types', () => {
spyOn(console, 'error');
var stub = <option>{1} <div /> {2}</option>;
stub = ReactTestUtils.renderIntoDocument(stub);
var node = ReactDOM.findDOMNode(stub);
var el = <option>{1} <div /> {2}</option>;
var node = ReactTestUtils.renderIntoDocument(el);
expect(node.innerHTML).toBe('1 2');
ReactTestUtils.renderIntoDocument(<option>{1} <div /> {2}</option>);
ReactTestUtils.renderIntoDocument(el);
// only warn once
expectDev(console.error.calls.count()).toBe(1);
expectDev(console.error.calls.argsFor(0)[0]).toContain(
'Only strings and numbers are supported as <option> children.'
expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toContain(
ReactDOMFeatureFlags.useFiber ?
'<div> cannot appear as a child of <option>.\n' +
' in div (at **)\n' +
' in option (at **)' :
'Only strings and numbers are supported as <option> children.'
);
});
+9 -6
View File
@@ -29,6 +29,7 @@ const ReactPortal = require('ReactPortal');
const UIManager = require('UIManager');
const deepFreezeAndThrowOnMutationInDev = require('deepFreezeAndThrowOnMutationInDev');
const emptyObject = require('emptyObject');
const findNodeHandle = require('findNodeHandle');
const invariant = require('invariant');
@@ -138,7 +139,7 @@ const NativeRenderer = ReactFiberReconciler({
type : string,
props : Props,
rootContainerInstance : Container,
hostContext : string | null,
hostContext : Object,
internalInstanceHandle : Object
) : Instance {
const tag = ReactNativeTagHandles.allocateTag();
@@ -174,6 +175,7 @@ const NativeRenderer = ReactFiberReconciler({
createTextInstance(
text : string,
rootContainerInstance : Container,
hostContext : Object,
internalInstanceHandle : Object,
) : TextInstance {
const tag = ReactNativeTagHandles.allocateTag();
@@ -210,11 +212,12 @@ const NativeRenderer = ReactFiberReconciler({
);
},
getChildHostContext(
parentHostContext : string | null,
type : string
) {
return parentHostContext;
getRootHostContext() {
return emptyObject;
},
getChildHostContext() {
return emptyObject;
},
insertBefore(
+12 -2
View File
@@ -27,6 +27,7 @@ var ReactInstanceMap = require('ReactInstanceMap');
var {
AnimationPriority,
} = require('ReactPriorityLevel');
var emptyObject = require('emptyObject');
var scheduledAnimationCallback = null;
var scheduledDeferredCallback = null;
@@ -40,8 +41,12 @@ var instanceCounter = 0;
var NoopRenderer = ReactFiberReconciler({
getRootHostContext() {
return emptyObject;
},
getChildHostContext() {
return null;
return emptyObject;
},
createInstance(type : string, props : Props) : Instance {
@@ -81,7 +86,12 @@ var NoopRenderer = ReactFiberReconciler({
resetTextContent(instance : Instance) : void {},
createTextInstance(text : string, rootContainerInstance : Container, internalInstanceHandle : Object) : TextInstance {
createTextInstance(
text : string,
rootContainerInstance : Container,
hostContext : Object,
internalInstanceHandle : Object
) : TextInstance {
var inst = { text : text, id: instanceCounter++ };
// Hide from unit tests
Object.defineProperty(inst, 'id', { value: inst.id, enumerable: false });
@@ -213,7 +213,8 @@ module.exports = function<T, P, I, TI, C, CX>(
newProps = workInProgress.memoizedProps || oldProps;
}
const instance : I = workInProgress.stateNode;
if (prepareUpdate(instance, type, oldProps, newProps)) {
const currentHostContext = getHostContext();
if (prepareUpdate(instance, type, oldProps, newProps, currentHostContext)) {
// This returns true if there was something to update.
markUpdate(workInProgress);
}
@@ -278,7 +279,8 @@ module.exports = function<T, P, I, TI, C, CX>(
}
}
const rootContainerInstance = getRootHostContainer();
const textInstance = createTextInstance(newText, rootContainerInstance, workInProgress);
const currentHostContext = getHostContext();
const textInstance = createTextInstance(newText, rootContainerInstance, currentHostContext, workInProgress);
workInProgress.stateNode = textInstance;
}
workInProgress.memoizedProps = newText;
@@ -17,7 +17,7 @@ import type { HostConfig } from 'ReactFiberReconciler';
export type HostContext<C, CX> = {
getRootHostContainer() : C,
getHostContext() : CX | null,
getHostContext() : CX,
pushHostContext(fiber : Fiber) : void,
popHostContext(fiber : Fiber) : void,
@@ -32,12 +32,13 @@ module.exports = function<T, P, I, TI, C, CX>(
) : HostContext<C, CX> {
const {
getChildHostContext,
getRootHostContext,
} = config;
// Context stack is reused across the subtrees.
// We use a null sentinel on the fiber stack to separate them.
let contextFibers : Array<Fiber | null> | null = null;
let contextValues : Array<CX | null> | null = null;
let contextFibers : Array<Fiber | null> = [];
let contextValues : Array<CX | null> = [];
let contextDepth : number = -1;
// Current context for fast access.
let currentContextValue : CX | null = null;
@@ -55,6 +56,7 @@ module.exports = function<T, P, I, TI, C, CX>(
}
function pushHostContainer(nextRootInstance : C) {
const nextRootContext = getRootHostContext(nextRootInstance);
if (rootInstance == null) {
// We're entering a root.
rootInstance = nextRootInstance;
@@ -64,14 +66,12 @@ module.exports = function<T, P, I, TI, C, CX>(
portalDepth++;
portalStack[portalDepth] = rootInstance;
rootInstance = nextRootInstance;
// Delimit subtree context with a sentinel so we know where to pop later.
if (contextFibers != null && contextValues != null) {
contextDepth++;
contextFibers[contextDepth] = null;
contextValues[contextDepth] = null;
currentContextValue = null;
}
}
// Push the next root or portal context.
contextDepth++;
contextFibers[contextDepth] = null;
contextValues[contextDepth] = nextRootContext;
currentContextValue = nextRootContext;
}
function popHostContainer() {
@@ -89,7 +89,7 @@ module.exports = function<T, P, I, TI, C, CX>(
// If we pushed any context while in a portal, we need to roll it back.
if (contextDepth > -1) {
contextDepth--;
if (contextDepth > -1 && contextValues != null) {
if (contextDepth > -1) {
currentContextValue = contextValues[contextDepth];
} else {
currentContextValue = null;
@@ -98,21 +98,21 @@ module.exports = function<T, P, I, TI, C, CX>(
}
}
function getHostContext() : CX | null {
function getHostContext() : CX {
if (currentContextValue == null) {
throw new Error('Expected host context to exist.');
}
return currentContextValue;
}
function pushHostContext(fiber : Fiber) : void {
const nextContextValue = getChildHostContext(currentContextValue, fiber.type);
if (currentContextValue == null) {
throw new Error('Expected root host context to exist.');
}
const nextContextValue = getChildHostContext(currentContextValue, fiber.type, rootInstance);
if (currentContextValue === nextContextValue) {
return;
}
if (contextFibers == null) {
contextFibers = [];
}
if (contextValues == null) {
contextValues = [];
}
contextDepth++;
contextFibers[contextDepth] = fiber;
contextValues[contextDepth] = nextContextValue;
@@ -144,7 +144,6 @@ module.exports = function<T, P, I, TI, C, CX>(
// Reset portal stack pointer because we're starting from the very top.
portalDepth = -1;
// Reset current container state.
// Don't reset arrays because we reuse them.
rootInstance = null;
contextDepth = -1;
currentContextValue = null;
@@ -40,19 +40,20 @@ type OpaqueNode = Fiber;
export type HostConfig<T, P, I, TI, C, CX> = {
getChildHostContext(parentHostContext : CX | null, type : T) : CX,
getRootHostContext(rootContainerInstance : C) : CX,
getChildHostContext(parentHostContext : CX, type : T) : CX,
createInstance(type : T, props : P, rootContainerInstance : C, hostContext : CX | null, internalInstanceHandle : OpaqueNode) : I,
createInstance(type : T, props : P, rootContainerInstance : C, hostContext : CX, internalInstanceHandle : OpaqueNode) : I,
appendInitialChild(parentInstance : I, child : I | TI) : void,
finalizeInitialChildren(parentInstance : I, type : T, props : P, rootContainerInstance : C) : void,
prepareUpdate(instance : I, type : T, oldProps : P, newProps : P) : boolean,
prepareUpdate(instance : I, type : T, oldProps : P, newProps : P, hostContext : CX) : boolean,
commitUpdate(instance : I, type : T, oldProps : P, newProps : P, rootContainerInstance : C, internalInstanceHandle : OpaqueNode) : void,
shouldSetTextContent(props : P) : boolean,
resetTextContent(instance : I) : void,
createTextInstance(text : string, rootContainerInstance : C, internalInstanceHandle : OpaqueNode) : TI,
createTextInstance(text : string, rootContainerInstance : C, hostContext : CX, internalInstanceHandle : OpaqueNode) : TI,
commitTextUpdate(textInstance : TI, oldText : string, newText : string) : void,
appendChild(parentInstance : I | C, child : I | TI) : void,