[DOM] Update HTML parser rules for new select parser
Project: facebook/react | Language: JavaScript | Concepts: api-design, error-handling, testing-strategy | Difficulty: mid
Link: https://github.com/react/react/pull/34804
The problem
React's DOM nesting validation logic was outdated with respect to recent changes in the HTML specification regarding <select>, <option>, and related elements. The spec update removed most special parsing rules for <select>, but React would still warn or error when rendering content that is now valid per the new spec and implemented in modern browsers. This led to unnecessary warnings and potential hydration errors for valid markup.
Why it's non-trivial
A naive fix would just remove the old rules or add new exceptions, but:
- The validation logic must match browser behavior across all supported browsers, not just the latest Chrome.
- The change impacts both runtime rendering and server-side hydration, so test coverage and error messaging must be carefully updated.
- The validator is designed to help developers avoid subtle cross-browser bugs, so relaxing rules too early could cause real-world issues for users on browsers that haven't yet shipped the new behavior.
The approach
1. Update the list of in-scope tags to include select:
const inScopeTags = [
'applet',
'caption',
'html',
'table',
'td',
'th',
'marquee',
'object',
+ 'select',
'template',
// ...
];
This change ensures that <select> is treated as an in-scope tag for DOM nesting validation, reflecting the new HTML parsing rules.
2. Remove special-case parsing logic for <select>, <optgroup>, and <option>:
- // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inselect
- case 'select':
- return (
- tag === 'hr' ||
- tag === 'option' ||
- tag === 'optgroup' ||
- tag === 'script' ||
- tag === 'template' ||
- tag === '#text'
- );
- case 'optgroup':
- return tag === 'option' || tag === '#text';
- // Strictly speaking, seeing an <option> doesn't mean we're in a <select>
- // but
- case 'option':
- return tag === '#text';
All the special-case logic for <select>, <optgroup>, and <option> is deleted, so these tags are now validated using the general rules, matching the new HTML spec and browser behavior.
3. Add a new rule: <input> cannot be a child of <select>:
+ case 'input':
+ // <input> causes open <select> tags to close.
+ return parentTag !== 'select';
This enforces that <input> elements are not allowed as children of <select>, which is still invalid per the spec.
4. Update test cases to match new behavior:
- Allow
<div>as a child of<option>without warning:
- it('should warn for invalid child tags', async () => {
+ it('should not warn for a child div tag', async () => {
const el = (
<option value="12">
{1} <div /> {2}
</option>
);
const container = await renderIntoDocument(el);
- assertConsoleErrorDev([
- 'In HTML, <div> cannot be a child of <option>.\n' +
- 'This will cause a hydration error.\n' +
- '\n' +
- '> <option value="12">\n' +
- '> <div>\n' +
- ' ...\n' +
- '\n' +
- ' in div (at **)',
- ]);
expect(container.firstChild.innerHTML).toBe('1 <div></div> 2');
await renderIntoDocument(el);
});
This test now expects no warning when a <div> is placed inside an <option>, in line with the new spec.
- Add a test to ensure
<input>inside<select>still warns:
+ expectWarnings(
+ ['select', 'input'],
+ [
+ 'In HTML, <input> cannot be a child of <select>.\n' +
+ 'This will cause a hydration error.\n' +
+ ' in input (at **)',
+ ],
+ );
This confirms that the new <input> rule is enforced.
From the review thread
The main purpose of the validation in
validateDOMNesting.jsis to provide a nicer reason for why something later goes wrong when there's a hydration error due to the parsing not lining up with React's expectations. The later error can't directly be attributed to this as a cause.It's a development time error so that you don't end up having to deduce later from production errors what might have gone wrong.
It's also an error even for client updates/navigations, even if you're not SSR:ing so that your component is resilient to being SSR:ed in the future, on a different page or if you happen to refresh the current page. It provides consistency in the ecosystem even in specific scenarios where it doesn't really come into play.
If you're developing in Chrome but deploying to production to users in Safari, it's still important to see this error in development even in Chrome. So I don't think we can take this as is until the fix is more widely distributed and enabled everywhere.
Thanks. So once the parser changes are shipped in Firefox and Safari then we can merge this?
I wonder if it would be worth tweaking the message to show that its valid in some browsers but not others yet
Takeaways
- Validation logic should match the lowest common denominator of browser support, not just the latest spec or one browser's implementation.
- When updating error-handling or validation code, always update and expand test coverage to match the new rules and edge cases.
- Error messages and validation should help developers avoid cross-browser inconsistencies, even if that means being conservative until all major browsers converge.