Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve validateDOMNesting message for whitespace #7515

Merged
merged 1 commit into from
Aug 19, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions src/renderers/dom/client/validateDOMNesting.js
Original file line number Diff line number Diff line change
Expand Up @@ -322,11 +322,24 @@ if (__DEV__) {

var didWarn = {};

validateDOMNesting = function(childTag, childInstance, ancestorInfo) {
validateDOMNesting = function(
childTag,
childText,
childInstance,
ancestorInfo
) {
ancestorInfo = ancestorInfo || emptyAncestorInfo;
var parentInfo = ancestorInfo.current;
var parentTag = parentInfo && parentInfo.tag;

if (childText != null) {
warning(
childTag == null,
'validateDOMNesting: when childText is passed, childTag should be null'
);
childTag = '#text';
}

var invalidParent =
isTagValidWithParent(childTag, parentTag) ? null : parentInfo;
var invalidAncestor =
Expand Down Expand Up @@ -385,7 +398,17 @@ if (__DEV__) {
didWarn[warnKey] = true;

var tagDisplayName = childTag;
if (childTag !== '#text') {
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 ' +
Copy link
Contributor

@mnpenner mnpenner Aug 18, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this message be confusing? Whitespace doesn't matter between JSX tags -- doesn't this problem only occur when there's whitespace inside a variable?

Edit: Nevermind, per this blog post, whitespace can appear when:

Element nodes will maintain white space when mixed with with non-element nodes on the same physical line of JSX code.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whitespace between tags on a single line matters, and I believe that's the most common cause of extra whitespace. See "Foo" in the test case below.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha, didn't know that. I guess that makes sense, but it's not something you normally think about! (Which is why this warning is helpful!)

'each line of your source code.';
}
} else {
tagDisplayName = '<' + childTag + '>';
}

Expand All @@ -398,10 +421,11 @@ if (__DEV__) {
}
warning(
false,
'validateDOMNesting(...): %s cannot appear as a child of <%s>. ' +
'validateDOMNesting(...): %s cannot appear as a child of <%s>.%s ' +
'See %s.%s',
tagDisplayName,
ancestorTag,
whitespaceInfo,
ownerInfo,
info
);
Expand Down
19 changes: 10 additions & 9 deletions src/renderers/dom/shared/ReactDOMComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,9 +254,9 @@ function optionPostMount() {
ReactDOMOption.postMountWrapper(inst);
}

var setContentChildForInstrumentation = emptyFunction;
var setAndValidateContentChildDev = emptyFunction;
if (__DEV__) {
setContentChildForInstrumentation = function(content) {
setAndValidateContentChildDev = function(content) {
var hasExistingContent = this._contentDebugID != null;
var debugID = this._debugID;
// This ID represents the inlined child that has no backing instance:
Expand All @@ -270,6 +270,7 @@ if (__DEV__) {
return;
}

validateDOMNesting(null, String(content), this, this._ancestorInfo);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a good place to put it? Since the function is called setContentChildForInstrumentation I wouldn’t expect other logic to show up here, or maybe we can rename the function itself.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll rename it.

this._contentDebugID = contentDebugID;
if (hasExistingContent) {
ReactInstrumentation.debugTool.onBeforeUpdateComponent(contentDebugID, content);
Expand Down Expand Up @@ -497,7 +498,7 @@ function ReactDOMComponent(element) {
this._flags = 0;
if (__DEV__) {
this._ancestorInfo = null;
setContentChildForInstrumentation.call(this, null);
setAndValidateContentChildDev.call(this, null);
}
}

Expand Down Expand Up @@ -605,7 +606,7 @@ ReactDOMComponent.Mixin = {
if (parentInfo) {
// parentInfo should always be present except for the top-level
// component when server rendering
validateDOMNesting(this._tag, this, parentInfo);
validateDOMNesting(this._tag, null, this, parentInfo);
}
this._ancestorInfo =
validateDOMNesting.updatedAncestorInfo(parentInfo, this._tag, this);
Expand Down Expand Up @@ -801,7 +802,7 @@ ReactDOMComponent.Mixin = {
// TODO: Validate that text is allowed as a child of this node
ret = escapeTextContentForBrowser(contentToUse);
if (__DEV__) {
setContentChildForInstrumentation.call(this, contentToUse);
setAndValidateContentChildDev.call(this, contentToUse);
}
} else if (childrenToUse != null) {
var mountImages = this.mountChildren(
Expand Down Expand Up @@ -843,7 +844,7 @@ ReactDOMComponent.Mixin = {
if (contentToUse != null) {
// TODO: Validate that text is allowed as a child of this node
if (__DEV__) {
setContentChildForInstrumentation.call(this, contentToUse);
setAndValidateContentChildDev.call(this, contentToUse);
}
DOMLazyTree.queueText(lazyTree, contentToUse);
} else if (childrenToUse != null) {
Expand Down Expand Up @@ -1117,7 +1118,7 @@ ReactDOMComponent.Mixin = {
if (lastContent !== nextContent) {
this.updateTextContent('' + nextContent);
if (__DEV__) {
setContentChildForInstrumentation.call(this, nextContent);
setAndValidateContentChildDev.call(this, nextContent);
}
}
} else if (nextHtml != null) {
Expand All @@ -1129,7 +1130,7 @@ ReactDOMComponent.Mixin = {
}
} else if (nextChildren != null) {
if (__DEV__) {
setContentChildForInstrumentation.call(this, null);
setAndValidateContentChildDev.call(this, null);
}

this.updateChildren(nextChildren, transaction, context);
Expand Down Expand Up @@ -1196,7 +1197,7 @@ ReactDOMComponent.Mixin = {
this._wrapperState = null;

if (__DEV__) {
setContentChildForInstrumentation.call(this, null);
setAndValidateContentChildDev.call(this, null);
}
},

Expand Down
2 changes: 1 addition & 1 deletion src/renderers/dom/shared/ReactDOMTextComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Object.assign(ReactDOMTextComponent.prototype, {
if (parentInfo) {
// parentInfo should always be present except for the top-level
// component when server rendering
validateDOMNesting('#text', this, parentInfo);
validateDOMNesting(null, this._stringText, this, parentInfo);
}
}

Expand Down
16 changes: 11 additions & 5 deletions src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -736,7 +736,7 @@ describe('ReactDOMComponent', function() {
});

it('should work error event on <source> element', function() {
spyOn(console, 'error');
spyOn(console, 'error');
var container = document.createElement('div');
ReactDOM.render(
<video>
Expand Down Expand Up @@ -1225,7 +1225,7 @@ describe('ReactDOMComponent', function() {

class Row extends React.Component {
render() {
return <tr />;
return <tr>x</tr>;
}
}

Expand All @@ -1237,15 +1237,21 @@ describe('ReactDOMComponent', function() {

ReactTestUtils.renderIntoDocument(<Foo />);

expect(console.error.calls.count()).toBe(2);
expect(console.error.calls.count()).toBe(3);
expect(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.'
);
expect(console.error.calls.argsFor(1)[0]).toBe(
'Warning: validateDOMNesting(...): #text cannot appear as a child ' +
'of <table>. See Foo > table > #text.'
'Warning: validateDOMNesting(...): Text nodes cannot appear as a ' +
'child of <tr>. See Row > tr > #text.'
);
expect(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.'
);
});

Expand Down