dangerouslySetInnerHTML is React's deliberately alarming-named escape hatch for injecting raw HTML, bypassing default XSS protection. This lesson covers its unusual syntax, why sanitization with a library like DOMPurify is almost always required, and the one genuinely safe exception.
1The Name Is a Warning, Not a Suggestion
React's team deliberately named this API dangerouslySetInnerHTML instead of a neutral name like setInnerHTML โ the awkward, alarming naming is intentional, forcing developers to consciously acknowledge they're bypassing React's default XSS protection every time they use it.
2The Syntax: An Object with __html
Unlike a normal prop, dangerouslySetInnerHTML requires an object with a specific __html key rather than a plain string. This unusual shape is deliberate, adding a small amount of friction that makes accidentally setting it without thinking less likely.
3You Almost Always Need Sanitization First
If an HTML string originates from anywhere outside hardcoded source code โ a CMS, a user submission, a third-party API โ it must be sanitized with a library like DOMPurify before being passed to dangerouslySetInnerHTML, stripping dangerous tags and attributes while preserving safe formatting.
4The Only Truly Safe Exception: Hardcoded, Static HTML
The one genuinely low-risk case is HTML that's a hardcoded string literal written directly in source code, since it can never contain a runtime XSS payload. The moment any variable or external value is mixed into that string, sanitization becomes mandatory again.
5Step-by-Step Breakdown
The Name Is a Warning, Not a Suggestion. React's team deliberately named this API dangerouslySetInnerHTML instead of something neutral like setInnerHTML โ the awkward, alarming name is intentional, forcing you to consciously acknowledge you're bypassing React's default XSS protection every single time you write it.
The Syntax: An Object with __html. Unlike a normal prop, dangerouslySetInnerHTML requires an object with a specific __html key, not a plain string. This isn't arbitrary โ the extra object wrapper makes it slightly harder to set accidentally, adding one more small speed bump before raw HTML gets injected.
Why does dangerouslySetInnerHTML require {{ __html: htmlString }} instead of accepting the string directly?
- โThe unusual wrapper adds deliberate friction, making accidental misuse less likely
- โIt's required purely for a rendering performance optimization
You Almost Always Need Sanitization First. If the HTML string comes from ANYWHERE outside your own hardcoded source code โ a CMS, a user submission, a third-party API โ it must be sanitized with a library like DOMPurify before being passed to dangerouslySetInnerHTML. Sanitizing strips dangerous tags and attributes while keeping safe formatting intact.
The Only Truly Safe Exception: Hardcoded, Static HTML. The one genuinely low-risk case is HTML that's a hardcoded string literal written directly in your own source code โ it can never contain a runtime XSS payload because it never changes based on external input. The moment ANY variable or external value gets concatenated into that string, sanitization becomes mandatory again.
Mastery Achieved. You now understand dangerouslySetInnerHTML: why its name is intentionally alarming, its unusual {{ __html: ... }} syntax, why sanitization with a library like DOMPurify is required for any content that isn't a fully hardcoded string, and exactly how narrow the truly safe exception is. Next, you'll learn how to build secure authentication UI.
Level Up ๐
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Sanitizers Can Also Strip Accessibility-Relevant Attributes
Some default DOMPurify configurations strip ARIA attributes by default โ if injected content relies on ARIA for accessibility, explicitly allow the needed attributes in the sanitizer configuration rather than losing them silently.
SEO Implications
- 1
Unsanitized Injected Content Risks Both XSS and Malformed Markup
Beyond the security risk, unsanitized HTML can also produce malformed markup that confuses crawlers โ sanitization helps ensure injected content remains well-formed, valid HTML.
Best Practices
Sanitize as Close to the Injection Point as Possible
Rather than trusting that data was sanitized somewhere upstream (like on the server), sanitize immediately before passing content to dangerouslySetInnerHTML, so the safety guarantee doesn't depend on trusting every other part of the pipeline.
Configure the Sanitizer's Allowlist Deliberately
Explicitly configure which tags and attributes DOMPurify allows based on actual formatting needs, rather than relying purely on defaults, to keep the injected content's capability surface as narrow as genuinely necessary.
Frequent Bugs
A rich-text CMS field renders correctly most of the time but occasionally allows an unexpected script to execute.
The CMS content is being passed to dangerouslySetInnerHTML without sanitization. Add DOMPurify.sanitize() (or an equivalent) immediately before the content reaches dangerouslySetInnerHTML.
Passing a plain string directly to dangerouslySetInnerHTML throws a runtime error.
dangerouslySetInnerHTML requires an object with a __html key, not a plain string. Wrap the string as { __html: htmlString }.
Real-World Examples
Sanitizing CMS-Sourced Blog Post Content
A blog renders post content authored in a CMS as rich HTML, since the CMS's editor produces formatted markup rather than Markdown. Because that content originates from a CMS (an external source, even if internally trusted), it's run through DOMPurify.sanitize() with an explicit allowlist of formatting tags before being passed to dangerouslySetInnerHTML.
function BlogPost({ htmlContent }) {
const clean = DOMPurify.sanitize(htmlContent, {
ALLOWED_TAGS: ['p', 'strong', 'em', 'a', 'ul', 'li', 'h2', 'h3'],
});
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}