Skip to content

Commit 4be3776

Browse files
sophiebitszpao
authored andcommitted
Improve validateDOMNesting message for whitespace (#7515)
For #5071. (cherry picked from commit 6a65960)
1 parent 8898803 commit 4be3776

File tree

4 files changed

+49
-18
lines changed

4 files changed

+49
-18
lines changed

src/renderers/dom/client/validateDOMNesting.js

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -322,11 +322,24 @@ if (__DEV__) {
322322

323323
var didWarn = {};
324324

325-
validateDOMNesting = function(childTag, childInstance, ancestorInfo) {
325+
validateDOMNesting = function(
326+
childTag,
327+
childText,
328+
childInstance,
329+
ancestorInfo
330+
) {
326331
ancestorInfo = ancestorInfo || emptyAncestorInfo;
327332
var parentInfo = ancestorInfo.current;
328333
var parentTag = parentInfo && parentInfo.tag;
329334

335+
if (childText != null) {
336+
warning(
337+
childTag == null,
338+
'validateDOMNesting: when childText is passed, childTag should be null'
339+
);
340+
childTag = '#text';
341+
}
342+
330343
var invalidParent =
331344
isTagValidWithParent(childTag, parentTag) ? null : parentInfo;
332345
var invalidAncestor =
@@ -385,7 +398,17 @@ if (__DEV__) {
385398
didWarn[warnKey] = true;
386399

387400
var tagDisplayName = childTag;
388-
if (childTag !== '#text') {
401+
var whitespaceInfo = '';
402+
if (childTag === '#text') {
403+
if (/\S/.test(childText)) {
404+
tagDisplayName = 'Text nodes';
405+
} else {
406+
tagDisplayName = 'Whitespace text nodes';
407+
whitespaceInfo =
408+
' Make sure you don\'t have any extra whitespace between tags on ' +
409+
'each line of your source code.';
410+
}
411+
} else {
389412
tagDisplayName = '<' + childTag + '>';
390413
}
391414

@@ -398,10 +421,11 @@ if (__DEV__) {
398421
}
399422
warning(
400423
false,
401-
'validateDOMNesting(...): %s cannot appear as a child of <%s>. ' +
424+
'validateDOMNesting(...): %s cannot appear as a child of <%s>.%s ' +
402425
'See %s.%s',
403426
tagDisplayName,
404427
ancestorTag,
428+
whitespaceInfo,
405429
ownerInfo,
406430
info
407431
);

src/renderers/dom/shared/ReactDOMComponent.js

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -253,9 +253,9 @@ function optionPostMount() {
253253
ReactDOMOption.postMountWrapper(inst);
254254
}
255255

256-
var setContentChildForInstrumentation = emptyFunction;
256+
var setAndValidateContentChildDev = emptyFunction;
257257
if (__DEV__) {
258-
setContentChildForInstrumentation = function(content) {
258+
setAndValidateContentChildDev = function(content) {
259259
var hasExistingContent = this._contentDebugID != null;
260260
var debugID = this._debugID;
261261
// This ID represents the inlined child that has no backing instance:
@@ -269,6 +269,7 @@ if (__DEV__) {
269269
return;
270270
}
271271

272+
validateDOMNesting(null, String(content), this, this._ancestorInfo);
272273
this._contentDebugID = contentDebugID;
273274
if (hasExistingContent) {
274275
ReactInstrumentation.debugTool.onBeforeUpdateComponent(contentDebugID, content);
@@ -492,7 +493,7 @@ function ReactDOMComponent(element) {
492493
this._flags = 0;
493494
if (__DEV__) {
494495
this._ancestorInfo = null;
495-
setContentChildForInstrumentation.call(this, null);
496+
setAndValidateContentChildDev.call(this, null);
496497
}
497498
}
498499

@@ -598,7 +599,7 @@ ReactDOMComponent.Mixin = {
598599
if (parentInfo) {
599600
// parentInfo should always be present except for the top-level
600601
// component when server rendering
601-
validateDOMNesting(this._tag, this, parentInfo);
602+
validateDOMNesting(this._tag, null, this, parentInfo);
602603
}
603604
this._ancestorInfo =
604605
validateDOMNesting.updatedAncestorInfo(parentInfo, this._tag, this);
@@ -794,7 +795,7 @@ ReactDOMComponent.Mixin = {
794795
// TODO: Validate that text is allowed as a child of this node
795796
ret = escapeTextContentForBrowser(contentToUse);
796797
if (__DEV__) {
797-
setContentChildForInstrumentation.call(this, contentToUse);
798+
setAndValidateContentChildDev.call(this, contentToUse);
798799
}
799800
} else if (childrenToUse != null) {
800801
var mountImages = this.mountChildren(
@@ -836,7 +837,7 @@ ReactDOMComponent.Mixin = {
836837
if (contentToUse != null) {
837838
// TODO: Validate that text is allowed as a child of this node
838839
if (__DEV__) {
839-
setContentChildForInstrumentation.call(this, contentToUse);
840+
setAndValidateContentChildDev.call(this, contentToUse);
840841
}
841842
DOMLazyTree.queueText(lazyTree, contentToUse);
842843
} else if (childrenToUse != null) {
@@ -1110,7 +1111,7 @@ ReactDOMComponent.Mixin = {
11101111
if (lastContent !== nextContent) {
11111112
this.updateTextContent('' + nextContent);
11121113
if (__DEV__) {
1113-
setContentChildForInstrumentation.call(this, nextContent);
1114+
setAndValidateContentChildDev.call(this, nextContent);
11141115
}
11151116
}
11161117
} else if (nextHtml != null) {
@@ -1122,7 +1123,7 @@ ReactDOMComponent.Mixin = {
11221123
}
11231124
} else if (nextChildren != null) {
11241125
if (__DEV__) {
1125-
setContentChildForInstrumentation.call(this, null);
1126+
setAndValidateContentChildDev.call(this, null);
11261127
}
11271128

11281129
this.updateChildren(nextChildren, transaction, context);
@@ -1185,7 +1186,7 @@ ReactDOMComponent.Mixin = {
11851186
this._wrapperState = null;
11861187

11871188
if (__DEV__) {
1188-
setContentChildForInstrumentation.call(this, null);
1189+
setAndValidateContentChildDev.call(this, null);
11891190
}
11901191
},
11911192

src/renderers/dom/shared/ReactDOMTextComponent.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ Object.assign(ReactDOMTextComponent.prototype, {
7575
if (parentInfo) {
7676
// parentInfo should always be present except for the top-level
7777
// component when server rendering
78-
validateDOMNesting('#text', this, parentInfo);
78+
validateDOMNesting(null, this._stringText, this, parentInfo);
7979
}
8080
}
8181

src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -777,7 +777,7 @@ describe('ReactDOMComponent', function() {
777777
});
778778

779779
it('should work error event on <source> element', function() {
780-
spyOn(console, 'error');
780+
spyOn(console, 'error');
781781
var container = document.createElement('div');
782782
ReactDOM.render(
783783
<video>
@@ -1224,7 +1224,7 @@ describe('ReactDOMComponent', function() {
12241224

12251225
class Row extends React.Component {
12261226
render() {
1227-
return <tr />;
1227+
return <tr>x</tr>;
12281228
}
12291229
}
12301230

@@ -1236,15 +1236,21 @@ describe('ReactDOMComponent', function() {
12361236

12371237
ReactTestUtils.renderIntoDocument(<Foo />);
12381238

1239-
expect(console.error.calls.count()).toBe(2);
1239+
expect(console.error.calls.count()).toBe(3);
12401240
expect(console.error.calls.argsFor(0)[0]).toBe(
12411241
'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
12421242
'<table>. See Foo > table > Row > tr. Add a <tbody> to your code to ' +
12431243
'match the DOM tree generated by the browser.'
12441244
);
12451245
expect(console.error.calls.argsFor(1)[0]).toBe(
1246-
'Warning: validateDOMNesting(...): #text cannot appear as a child ' +
1247-
'of <table>. See Foo > table > #text.'
1246+
'Warning: validateDOMNesting(...): Text nodes cannot appear as a ' +
1247+
'child of <tr>. See Row > tr > #text.'
1248+
);
1249+
expect(console.error.calls.argsFor(2)[0]).toBe(
1250+
'Warning: validateDOMNesting(...): Whitespace text nodes cannot ' +
1251+
'appear as a child of <table>. Make sure you don\'t have any extra ' +
1252+
'whitespace between tags on each line of your source code. See Foo > ' +
1253+
'table > #text.'
12481254
);
12491255
});
12501256

0 commit comments

Comments
 (0)