Prop `dangerouslySetInnerHTML` did not match
Have you ever gotten the following error when using dangerouslySetInnerHTML
?
Hello there</p>
I see this most often when displaying parsed Markdown in Next.js and Remix apps. The usual culprit looks something like this
<p dangerouslySetInnerHTML={{ __html: post.body }} />
Quick solution
Change the <p>
to a <div>
Why that works
When React runs the above JSX, the following happens:
1. The JSX is server rendered into the following HTML
<p>
<p>Hello there</p>
</p>
2. That HTML string is sent to the browser, which creates a corresponding in-memory DOM tree, which it uses to render the page. This DOM tree is what you see in the DevTools “Inspect” tab. This is where the problem happens.
3. React hydrates the DOM tree by re-executing your JSX on the client and comparing it with the in-memory DOM tree.
The server-executed and client-executed JSX create the same HTML structure, so why is there a mismatch?
Some HTML elements, like the <p>
tag, can't nest inside each other. When the browser encounters a nested opening <p>
tag, it assumes that you meant to close the original <p>
tag before opening the new one. So this HTML
<p>
<p>Hello there</p>
</p>
becomes this in the DOM of the web page
<p>
</p><p>Hello there</p><p>
</p>
However, when React constructs the DOM in the browser using Javascript, it results in a DOM structure that does have nested <p>
tags.
const outerP = document.createElement('p');
const innerP = document.createElement('p');
innerP.textContent = 'Hello there';
outerP.appendChild(innerP);
// Creates this DOM structure
// <p>
// <p>Hello there</p>
// </p>
When React performs step 3 above, it compares the 3 sibling <p>
tags from the server with the nested <p>
tags from the client-side JSX and sees a mismatch, giving you a console error.
Why does dangerouslySetInnerHTML
not work on <p>
elements? Server/client mismatch.
Other scenarios where this happens
The HTML spec specifies a list of 49 "Phrasing elements" that restrict what other content is allowed as children. However, not all of these elements exhibit the behavior described above. We can test for the behavior with the following code snippet:
[
'a', 'abbr', 'audio', 'b', 'button', 'canvas',
'cite', 'code', 'data', 'datalist', 'del', 'dfn',
'em', 'embed', 'i', 'iframe', 'img', 'input', 'ins',
'kbd', 'label', 'map', 'mark', 'meter', 'noscript',
'object', 'output', 'picture', 'progress', 'q',
'ruby', 's', 'samp', 'script', 'select', 'slot',
'small', 'span', 'strong', 'sub', 'sup', 'template',
'textarea', 'time', 'u', 'p', 'var', 'video', 'wbr'
].filter(function elementCanNotContainItself (tagName) {
// create wrapper element
const wrap = document.createElement('div');
const ele = document.createElement(tagName);
wrap.appendChild(ele);
// create nested element and append
ele.append(document.createElement(tagName));
// pull out resulting innerHTML that keeps elements nested
const initInnerHTML = wrap.innerHTML;
wrap.innerHTML = '';
// re-apply nested elements, allow the HTML parser to auto-close
// tag if necessary
wrap.innerHTML = initInnerHTML;
// compare the result of the HTML parser with the forced-nesting result
if (wrap.innerHTML !== initInnerHTML) {
return true;
} else {
return false;
}
})
// > ['a', 'button', 'iframe', 'noscript', 'script', 'select', 'textarea', 'p']
For each of the returned elements, <a>
, <button>
, <iframe>
, <noscript>
, <script>
, <select>
, <textarea>
*, and <p>
, React will encounter a hydration error if the element has a dangerouslySetInnerHTML
prop that tries to add itself as a nested element.
I can't imagine a scenario in which you would want to use dangerouslySetInnerHTML
on most of those elements, but it's good to know that this issue doesn't just happen to the <p>
tag.
* The <textarea>
error is different from other errors. When creating a self-closing <textarea>
tag in React, React implicitly gives the element an empty string as the children to initialize the input. Because of this, React gives the error Error: Can only set one of `children` or `props.dangerouslySetInnerHTML`
.