Skip to content

Commit ac43bf6

Browse files
authored
Move validation of text nesting into ReactDOMComponent (#26594)
Extract validateTextNesting from validateDOMNesting. We only need the parent tag when validating text nodes. Then validate it in setProp.
1 parent ca41adb commit ac43bf6

File tree

4 files changed

+102
-64
lines changed

4 files changed

+102
-64
lines changed

packages/react-dom-bindings/src/client/ReactDOMComponent.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
updateTextarea,
4646
restoreControlledTextareaState,
4747
} from './ReactDOMTextarea';
48+
import {validateTextNesting} from './validateDOMNesting';
4849
import {track} from './inputValueTracking';
4950
import setInnerHTML from './setInnerHTML';
5051
import setTextContent from './setTextContent';
@@ -279,6 +280,9 @@ function setProp(
279280
switch (key) {
280281
case 'children': {
281282
if (typeof value === 'string') {
283+
if (__DEV__) {
284+
validateTextNesting(value, tag);
285+
}
282286
// Avoid setting initial textContent when the text is empty. In IE11 setting
283287
// textContent on a <textarea> will cause the placeholder to not
284288
// show within the <textarea> until it has been focused and blurred again.
@@ -290,6 +294,9 @@ function setProp(
290294
setTextContent(domElement, value);
291295
}
292296
} else if (typeof value === 'number') {
297+
if (__DEV__) {
298+
validateTextNesting('' + value, tag);
299+
}
293300
const canSetTextContent = !enableHostSingletons || tag !== 'body';
294301
if (canSetTextContent) {
295302
setTextContent(domElement, '' + value);

packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,11 @@ import {
5959
} from './ReactDOMComponent';
6060
import {getSelectionInformation, restoreSelection} from './ReactInputSelection';
6161
import setTextContent from './setTextContent';
62-
import {validateDOMNesting, updatedAncestorInfoDev} from './validateDOMNesting';
62+
import {
63+
validateDOMNesting,
64+
validateTextNesting,
65+
updatedAncestorInfoDev,
66+
} from './validateDOMNesting';
6367
import {
6468
isEnabled as ReactBrowserEventEmitterIsEnabled,
6569
setEnabled as ReactBrowserEventEmitterSetEnabled,
@@ -328,18 +332,7 @@ export function createInstance(
328332
if (__DEV__) {
329333
// TODO: take namespace into account when validating.
330334
const hostContextDev: HostContextDev = (hostContext: any);
331-
validateDOMNesting(type, null, hostContextDev.ancestorInfo);
332-
if (
333-
typeof props.children === 'string' ||
334-
typeof props.children === 'number'
335-
) {
336-
const string = '' + props.children;
337-
const ownAncestorInfo = updatedAncestorInfoDev(
338-
hostContextDev.ancestorInfo,
339-
type,
340-
);
341-
validateDOMNesting(null, string, ownAncestorInfo);
342-
}
335+
validateDOMNesting(type, hostContextDev.ancestorInfo);
343336
namespace = hostContextDev.namespace;
344337
} else {
345338
const hostContextProd: HostContextProd = (hostContext: any);
@@ -491,21 +484,6 @@ export function prepareUpdate(
491484
// TODO: Figure out how to validateDOMNesting when children turn into a string.
492485
return null;
493486
}
494-
if (__DEV__) {
495-
const hostContextDev = ((hostContext: any): HostContextDev);
496-
if (
497-
typeof newProps.children !== typeof oldProps.children &&
498-
(typeof newProps.children === 'string' ||
499-
typeof newProps.children === 'number')
500-
) {
501-
const string = '' + newProps.children;
502-
const ownAncestorInfo = updatedAncestorInfoDev(
503-
hostContextDev.ancestorInfo,
504-
type,
505-
);
506-
validateDOMNesting(null, string, ownAncestorInfo);
507-
}
508-
}
509487
return diffProperties(domElement, type, oldProps, newProps);
510488
}
511489

@@ -529,7 +507,10 @@ export function createTextInstance(
529507
): TextInstance {
530508
if (__DEV__) {
531509
const hostContextDev = ((hostContext: any): HostContextDev);
532-
validateDOMNesting(null, text, hostContextDev.ancestorInfo);
510+
const ancestor = hostContextDev.ancestorInfo.current;
511+
if (ancestor != null) {
512+
validateTextNesting(text, ancestor.tag);
513+
}
533514
}
534515
const textNode: TextInstance = getOwnerDocumentFromRootContainer(
535516
rootContainerInstance,
@@ -1756,7 +1737,7 @@ export function resolveSingletonInstance(
17561737
if (__DEV__) {
17571738
const hostContextDev = ((hostContext: any): HostContextDev);
17581739
if (validateDOMNestingDev) {
1759-
validateDOMNesting(type, null, hostContextDev.ancestorInfo);
1740+
validateDOMNesting(type, hostContextDev.ancestorInfo);
17601741
}
17611742
}
17621743
const ownerDocument = getOwnerDocumentFromRootContainer(

packages/react-dom-bindings/src/client/validateDOMNesting.js

Lines changed: 33 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -434,29 +434,14 @@ function findInvalidAncestorForTag(
434434
const didWarn: {[string]: boolean} = {};
435435

436436
function validateDOMNesting(
437-
childTag: ?string,
438-
childText: ?string,
437+
childTag: string,
439438
ancestorInfo: AncestorInfoDev,
440439
): void {
441440
if (__DEV__) {
442441
ancestorInfo = ancestorInfo || emptyAncestorInfoDev;
443442
const parentInfo = ancestorInfo.current;
444443
const parentTag = parentInfo && parentInfo.tag;
445444

446-
if (childText != null) {
447-
if (childTag != null) {
448-
console.error(
449-
'validateDOMNesting: when childText is passed, childTag should be null',
450-
);
451-
}
452-
childTag = '#text';
453-
} else if (childTag == null) {
454-
console.error(
455-
'validateDOMNesting: when childText or childTag must be provided',
456-
);
457-
return;
458-
}
459-
460445
const invalidParent = isTagValidWithParent(childTag, parentTag)
461446
? null
462447
: parentInfo;
@@ -478,21 +463,7 @@ function validateDOMNesting(
478463
}
479464
didWarn[warnKey] = true;
480465

481-
let tagDisplayName = childTag;
482-
let whitespaceInfo = '';
483-
if (childTag === '#text') {
484-
if (childText != null && /\S/.test(childText)) {
485-
tagDisplayName = 'Text nodes';
486-
} else {
487-
tagDisplayName = 'Whitespace text nodes';
488-
whitespaceInfo =
489-
" Make sure you don't have any extra whitespace between tags on " +
490-
'each line of your source code.';
491-
}
492-
} else {
493-
tagDisplayName = '<' + childTag + '>';
494-
}
495-
466+
const tagDisplayName = '<' + childTag + '>';
496467
if (invalidParent) {
497468
let info = '';
498469
if (ancestorTag === 'table' && childTag === 'tr') {
@@ -501,10 +472,9 @@ function validateDOMNesting(
501472
'the browser.';
502473
}
503474
console.error(
504-
'validateDOMNesting(...): %s cannot appear as a child of <%s>.%s%s',
475+
'validateDOMNesting(...): %s cannot appear as a child of <%s>.%s',
505476
tagDisplayName,
506477
ancestorTag,
507-
whitespaceInfo,
508478
info,
509479
);
510480
} else {
@@ -518,4 +488,33 @@ function validateDOMNesting(
518488
}
519489
}
520490

521-
export {updatedAncestorInfoDev, validateDOMNesting};
491+
function validateTextNesting(childText: string, parentTag: string): void {
492+
if (__DEV__) {
493+
if (isTagValidWithParent('#text', parentTag)) {
494+
return;
495+
}
496+
497+
// eslint-disable-next-line react-internal/safe-string-coercion
498+
const warnKey = '#text|' + parentTag;
499+
if (didWarn[warnKey]) {
500+
return;
501+
}
502+
didWarn[warnKey] = true;
503+
504+
if (/\S/.test(childText)) {
505+
console.error(
506+
'validateDOMNesting(...): Text nodes cannot appear as a child of <%s>.',
507+
parentTag,
508+
);
509+
} else {
510+
console.error(
511+
'validateDOMNesting(...): Whitespace text nodes cannot appear as a child of <%s>. ' +
512+
"Make sure you don't have any extra whitespace between tags on " +
513+
'each line of your source code.',
514+
parentTag,
515+
);
516+
}
517+
}
518+
}
519+
520+
export {updatedAncestorInfoDev, validateDOMNesting, validateTextNesting};

packages/react-dom/src/__tests__/ReactDOMComponent-test.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1855,6 +1855,57 @@ describe('ReactDOMComponent', () => {
18551855
]);
18561856
});
18571857

1858+
it('warns nicely for updating table rows to use text', () => {
1859+
const container = document.createElement('div');
1860+
1861+
function Row({children}) {
1862+
return <tr>{children}</tr>;
1863+
}
1864+
1865+
function Foo({children}) {
1866+
return <table>{children}</table>;
1867+
}
1868+
1869+
// First is fine.
1870+
ReactDOM.render(<Foo />, container);
1871+
1872+
expect(() => ReactDOM.render(<Foo> </Foo>, container)).toErrorDev([
1873+
'Warning: validateDOMNesting(...): Whitespace text nodes cannot ' +
1874+
"appear as a child of <table>. Make sure you don't have any extra " +
1875+
'whitespace between tags on each line of your source code.' +
1876+
'\n in table (at **)' +
1877+
'\n in Foo (at **)',
1878+
]);
1879+
1880+
ReactDOM.render(
1881+
<Foo>
1882+
<tbody>
1883+
<Row />
1884+
</tbody>
1885+
</Foo>,
1886+
container,
1887+
);
1888+
1889+
expect(() =>
1890+
ReactDOM.render(
1891+
<Foo>
1892+
<tbody>
1893+
<Row>text</Row>
1894+
</tbody>
1895+
</Foo>,
1896+
container,
1897+
),
1898+
).toErrorDev([
1899+
'Warning: validateDOMNesting(...): Text nodes cannot appear as a ' +
1900+
'child of <tr>.' +
1901+
'\n in tr (at **)' +
1902+
'\n in Row (at **)' +
1903+
'\n in tbody (at **)' +
1904+
'\n in table (at **)' +
1905+
'\n in Foo (at **)',
1906+
]);
1907+
});
1908+
18581909
it('gives useful context in warnings', () => {
18591910
function Row() {
18601911
return <tr />;

0 commit comments

Comments
 (0)