The most surprising truth about preventing XSS in React is that the framework already does most of the heavy lifting for you, but only if you understand how it works and don’t accidentally disable it.

Let’s see React in action, preventing a common XSS attack. Imagine a user inputs the following string into a comment field:

<img src="invalid-image" onerror="alert('XSS Attack!');" />

If you were to directly inject this string into the DOM using innerHTML in plain JavaScript, the browser would execute the onerror event handler, popping up that dreaded alert box. However, in React, when you render this string as a JSX element like this:

function Comment({ text }) {
  return <div>{text}</div>;
}

// ... in another component
<Comment text='<img src="invalid-image" onerror="alert("XSS Attack!");" />' />

React automatically escapes the HTML. What you actually see rendered in the DOM is not an executable image tag, but the literal string:

<div>&lt;img src="invalid-image" onerror="alert("XSS Attack!");" /&gt;</div>

The < becomes &lt;, > becomes &gt;. The browser, seeing these HTML entities, renders them as text characters rather than interpreting them as HTML tags or JavaScript code. This is React’s built-in defense mechanism.

This automatic escaping happens for most common rendering scenarios, primarily when you’re rendering strings within JSX elements. React treats anything rendered directly within curly braces {} as text content, not as raw HTML. This is the core of its XSS prevention strategy: it defaults to treating user-provided input as data, not as executable code.

The problem arises when you bypass this default behavior. The most common way to do this is by using dangerouslySetInnerHTML. This prop, aptly named, allows you to inject raw HTML into a React element. If you were to use it with untrusted user input, you’d be opening the door to XSS:

function DangerousComment({ html }) {

  return <div dangerouslySetInnerHTML={{ __html: html }} />;

}

// If 'html' comes from user input and contains malicious script:
<DangerousComment html='<img src="invalid-image" onerror="alert("XSS Attack!");" />' />

This would execute the alert. The "dangerously" in the name is a serious warning. You should only use dangerouslySetInnerHTML when you absolutely trust the source of the HTML (e.g., it’s pre-sanitized server-side content you control) or after you’ve performed thorough sanitization on the client-side yourself.

Another common pitfall involves rendering user-provided data that might contain embedded HTML tags that you intend to render, but haven’t properly sanitized. For instance, if a user is allowed to format their input with basic HTML like <b>bold</b> and you want that to render as actual bold text, you might be tempted to use dangerouslySetInnerHTML. However, a malicious user could embed scripts within those tags. The correct approach here is to use a sanitization library. Libraries like dompurify can strip out dangerous tags and attributes while allowing safe ones.

import DOMPurify from 'dompurify';

function SafeComment({ html }) {
  const cleanHtml = DOMPurify.sanitize(html);

  return <div dangerouslySetInnerHTML={{ __html: cleanHtml }} />;

}

This ensures that only intended HTML is rendered, and any malicious code is removed.

Beyond dangerouslySetInnerHTML, be cautious with any method that bypasses React’s default text rendering. This includes manually manipulating the DOM outside of React’s lifecycle, or passing raw HTML strings to third-party libraries that then inject them into the DOM without proper sanitization. Always ensure that any HTML you’re injecting, especially from external sources, has been thoroughly vetted.

The mental model to hold is: React treats {variable} as text. If you need to render HTML, you must explicitly opt-in to dangerouslySetInnerHTML and take full responsibility for sanitizing the content.

Many developers miss that React’s attribute handling also provides protection. For example, if you were to render an href attribute with a javascript: URL, React would escape it. However, if you were to use dangerouslySetInnerHTML to inject an <a> tag with a javascript: href, that would execute. The principle remains the same: don’t let untrusted input become executable code.

The next challenge you’ll encounter is managing state updates efficiently when dealing with complex user-generated content.

Want structured learning?

Take the full React course →