How to Display Dynamic HTML in React

Beginner8 min interview
Skills tested:
Understanding that React escapes HTML strings by default to prevent XSSUsing dangerouslySetInnerHTML with the required __html key to render raw HTMLSanitizing untrusted HTML with DOMPurify before renderingKnowing when dangerouslySetInnerHTML is appropriate (CMS content, email templates, markdown-to-HTML) vs when it is notUnderstanding the XSS risk of rendering unsanitized user-generated HTML

Advertisement

🧩 Scenario

Blog post content from a CMS, email template previews, and markdown converted to HTML all arrive as HTML strings that must be injected into the DOM. React escapes HTML by default to prevent XSS. The dangerouslySetInnerHTML prop is the deliberate opt-out, and DOMPurify is the sanitizer that makes it safe for untrusted content.

Architecture Walkthrough

Why React Escapes HTML by Default

React escapes all string content before inserting it into the DOM. If you render <div>{htmlString}</div> and htmlString is "<b>Hello</b>", React outputs the literal text <b>Hello</b> in the browser, not bold text. This is intentional: it prevents XSS (Cross-Site Scripting), where a malicious actor injects executable script tags or event handler attributes into rendered content. By default, React treats all strings as plain text.

This protection covers the most common attack vector, where user-generated content is reflected back into the page.

dangerouslySetInnerHTML

When you need to render actual HTML markup from a trusted source (a CMS, a Markdown-to-HTML converter, an email preview), React provides dangerouslySetInnerHTML. The prop requires an object with a __html key, not a plain string. This naming is intentional friction: the double underscore and the word "dangerous" are signals to the developer that this is an opt-out from React's safety model, requiring deliberate thought.

The prop sets the element's innerHTML directly. It bypasses React's virtual DOM diffing for that element's children, and no child React elements can be placed alongside it on the same element.

Sanitization with DOMPurify

When the HTML source is untrusted (user input, API responses from unknown origins, third-party CMS content), you must sanitize before rendering. DOMPurify strips script tags, event handler attributes (onclick, onerror, etc.), and other XSS vectors while preserving safe formatting tags. Defense in depth means sanitizing on the server as well, not only on the client.


Key Code Explained

// Basic usage: trusted HTML from your own CMS
interface BlogPostProps {
  title: string;
  htmlContent: string; // HTML string from a trusted CMS
}

function BlogPost({ title, htmlContent }: BlogPostProps) {
  return (
    <article>
      <h1>{title}</h1>
      {/* dangerouslySetInnerHTML requires an object with __html key */}
      <div
        className="prose"
        dangerouslySetInnerHTML={{ __html: htmlContent }}
      />
    </article>
  );
}


// Sanitized usage: untrusted HTML from users or external APIs
import DOMPurify from 'dompurify';

interface CommentBodyProps {
  userHtml: string; // user-submitted content — always untrusted
}

function CommentBody({ userHtml }: CommentBodyProps) {
  // Sanitize before rendering: strips scripts, event handlers, dangerous attributes
  const sanitized = DOMPurify.sanitize(userHtml, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li'],
    ALLOWED_ATTR: ['href', 'target', 'rel'],
  });

  return (
    <div
      className="comment-body"
      dangerouslySetInnerHTML={{ __html: sanitized }}
    />
  );
}


// When to avoid dangerouslySetInnerHTML: prefer React components
// BAD: rendering a simple list as HTML string
const htmlList = '<ul><li>Apple</li><li>Banana</li></ul>';
<div dangerouslySetInnerHTML={{ __html: htmlList }} />

// GOOD: render it as React components — no XSS risk, React-managed
const fruits = ['Apple', 'Banana'];
<ul>
  {fruits.map((fruit) => <li key={fruit}>{fruit}</li>)}
</ul>


// Server-side sanitization (Node.js with jsdom for DOMPurify)
// src/lib/sanitize.ts
import createDOMPurify from 'dompurify';
import { JSDOM } from 'jsdom';

const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window as unknown as Window);

export function sanitizeHtml(dirty: string): string {
  return DOMPurify.sanitize(dirty);
}

The ALLOWED_TAGS and ALLOWED_ATTR configuration is intentional. Passing an allowlist to DOMPurify is safer than relying on the default blocklist, because new XSS vectors are discovered over time. An allowlist only permits what you explicitly need.


Tradeoffs

ApproachXSS safe by defaultUse when
JSX string interpolationYesAll plain text content
dangerouslySetInnerHTML (trusted)Depends on sourceCMS HTML, markdown-to-HTML from your own pipeline
dangerouslySetInnerHTML + DOMPurifyYes (when configured correctly)User-generated HTML, third-party content
React componentsYesAny structured content you control

What Interviewers Actually Check

  • Whether you know that React escapes HTML strings by default and why
  • Whether you know dangerouslySetInnerHTML requires { __html: string }, not a plain string
  • Whether you know to sanitize untrusted HTML with DOMPurify before rendering
  • Whether you can explain what XSS is and why it matters
  • Whether you know that dangerouslySetInnerHTML should be avoided when React components can do the job instead

Follow-Up Questions

  1. How does Content Security Policy (CSP) complement DOMPurify as a defense against XSS?
  2. What is the difference between stored XSS, reflected XSS, and DOM-based XSS, and which does dangerouslySetInnerHTML protect against?
  3. How would you render Markdown in React safely using a library like react-markdown instead of dangerouslySetInnerHTML?
  4. Can DOMPurify be used in a Node.js (server-side) environment, and what additional setup does it require?
  5. How does React 19's new innerHTML prop relate to dangerouslySetInnerHTML?

Common Candidate Mistakes

  • Passing user-generated HTML directly to dangerouslySetInnerHTML without sanitizing it, creating an XSS vulnerability
  • Not knowing the { __html: string } object shape and trying to pass a plain string
  • Using dangerouslySetInnerHTML for simple structured content that could be expressed as React components
  • Sanitizing only on the client, not knowing that server-side sanitization is also necessary for defense in depth
  • Thinking that DOMPurify with default settings is always sufficient, without knowing that a restrictive allowlist is safer

Interview Readiness Checklist

Before you leave this question, make sure you can answer:

  • Can you use dangerouslySetInnerHTML to render an HTML string from a CMS?
  • Can you integrate DOMPurify to sanitize HTML before rendering?
  • Can you explain why React escapes HTML by default and what XSS is?
  • Can you articulate when dangerouslySetInnerHTML is appropriate vs when React components are better?
  • Can you explain why the __html key exists and what the naming signals to developers?

Summary

React escapes all HTML strings by default, rendering them as literal text rather than markup. This prevents XSS, the most common injection attack where malicious script tags or event handler attributes are executed in the browser. When you genuinely need to inject HTML markup (CMS blog post bodies, markdown-to-HTML output, email previews), React provides dangerouslySetInnerHTML as a deliberate opt-out. The prop requires an object with a __html key, not a plain string, as an intentional signal that this is a conscious security decision.

For HTML from untrusted sources (user-generated content, third-party APIs), always sanitize with DOMPurify before passing to dangerouslySetInnerHTML. Configure DOMPurify with an explicit allowlist of permitted tags and attributes rather than relying on the default blocklist. Defense in depth means sanitizing on the server as well, not only in the client component.

Avoid dangerouslySetInnerHTML whenever the content can be expressed as React components. A list, a table, or a card layout from structured data should be rendered as JSX elements. dangerouslySetInnerHTML is for opaque HTML blobs from CMS and document pipelines, not for structured data you control.

Related Videos
Watch these videos to learn more about this topic
Frequently Asked Questions

Can I use innerHTML in React?

Not directly. React escapes all HTML by default. Use dangerouslySetInnerHTML with the __html key to opt out of escaping. Always sanitize external HTML before passing it in.

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Advertisement