Sooner or later, every React application runs into a string of HTML it did not write by hand. It comes back from a headless CMS, an API endpoint, a markdown-to-HTML pipeline, or a rich text editor. You need to render it, but you also do not want to surrender control the moment you do. That is the awkward spot where most of us reach for dangerouslySetInnerHTML, hold our breath, and accept that everything inside that blob is now invisible to React.
html-react-parser exists for exactly this problem. It takes an HTML string and returns genuine React elements rather than a chunk of opaque inner markup. Because the output is real React, you can intercept any node mid-parse, swap an <a> for your router's <Link>, turn an <img> into a custom lazy-loading component, strip out tags you do not trust, or sprinkle in className values. The library is small, works on both the server and the browser, and has become one of the most widely depended-on packages in the React ecosystem, pulling in roughly 3.6 million downloads a week.
Why Not Just Use dangerouslySetInnerHTML
The built-in approach has its place, but it comes with real limitations. When you assign HTML through dangerouslySetInnerHTML, React stops looking inside. Those nodes never become part of the virtual DOM in any meaningful way, so you cannot attach behavior, replace pieces, or reconcile them like normal components. You are essentially handing a sealed box to the browser.
html-react-parser keeps everything inside the React tree. It parses the string into a node structure and rebuilds it with React.createElement, which means the result behaves like any other JSX you would write yourself. You still need to sanitize untrusted input (more on that later), but you trade a sealed box for a fully inspectable, transformable tree. For content-driven applications, that difference is the whole game.
What You Get in the Box
The core of the library is a single function, but the surrounding options are where the power lives.
- Real React output. Parse single elements, multiple sibling elements, or deeply nested structures, all converted into standard React elements.
- A
replacehook. Inspect each parsed node and optionally return a different React element to take its place. - A
transformhook. Wrap or post-process every node after it has been converted. - Attribute conversion. A helper turns DOM attributes like
classand inlinestylestrings into proper React props. - Library swapping. React is the default, but you can point it at Preact or any compatible library.
- Server and client support. It runs happily under Node.js for SSR and in the browser, with no special configuration.
- A tiny footprint. Around 10 KB gzipped with only four dependencies.
Getting It Installed
The package is published under its plain name with no scope, so installation is straightforward.
npm install html-react-parser
Or with Yarn:
yarn add html-react-parser
The default export is the parser itself. In an ES module project you import it directly, and in a CommonJS context you reach for the .default key.
import parse from "html-react-parser";
// CommonJS equivalent
// const parse = require("html-react-parser").default;
From String to Elements in One Call
The simplest use is also the most common: hand the parser a string and render what comes back.
import parse from "html-react-parser";
function Article() {
const html = "<p>Hello, <strong>World</strong>!</p>";
return <div className="prose">{parse(html)}</div>;
}
That parse call returns a real React element, not a string and not an inner-HTML assignment. When the string contains several top-level elements, the parser returns an array, so you should render it inside a parent element to keep React happy.
function List() {
const html = "<li>Item 1</li><li>Item 2</li>";
return <ul>{parse(html)}</ul>;
}
Attributes come along for the ride. Classes, inline styles, data-* attributes, and custom attributes are all parsed and mapped to the appropriate React props automatically, so a string like <hr class="bar" style="top:42px;" data-attr="baz"> renders exactly as you would expect.
Rewriting Nodes With replace
The replace option is the feature most people come for. You pass a callback that receives each parsed DOM node. If you return a valid React element, it takes the place of the original; if you return nothing, the node is left untouched. This is how you bridge plain HTML and your own component system.
A classic example is upgrading anchor tags into your router's link component so that navigation stays client-side.
import parse, { HTMLReactParserOptions, Element } from "html-react-parser";
import { Link } from "react-router-dom";
const options: HTMLReactParserOptions = {
replace(domNode) {
if (
domNode instanceof Element &&
domNode.name === "a" &&
domNode.attribs.href
) {
return (
<Link to={domNode.attribs.href}>
{domNode.children.map((child) =>
child.type === "text" ? (child as any).data : null,
)}
</Link>
);
}
},
};
function Content({ html }: { html: string }) {
return <div>{parse(html, options)}</div>;
}
If you are using TypeScript, note the domNode instanceof Element check. The callback receives a general DOM node type, and only the Element subtype carries the attribs and name properties. Guarding with instanceof keeps the compiler happy and your code safe. The second argument to replace is an index, but be careful: it resets to zero when the parser descends into a node's children, so it is not safe to use as a unique React key.
Replacing Elements Along With Their Children
When you replace an element that has children, you usually want to keep those children and re-render them through the same options. The domToReact helper does exactly that, recursively converting child nodes while respecting your replacement rules.
import parse, { domToReact, Element } from "html-react-parser";
const options = {
replace(domNode: any) {
if (!(domNode instanceof Element) || !domNode.attribs) {
return;
}
if (domNode.attribs.id === "main") {
return (
<h1 style={{ fontSize: 42 }}>
{domToReact(domNode.children as any, options)}
</h1>
);
}
},
};
parse('<p id="main"><span>Make me a heading</span></p>', options);
Removing an element entirely is just as easy. Return an empty fragment and the node disappears from the output while its place in the tree is cleanly closed.
parse('<p><br id="remove"></p>', {
replace: ({ attribs }: any) => attribs?.id === "remove" && <></>,
});
There is also an attributesToProps helper for when you want to keep an element's existing attributes but render it through a different tag. It converts the raw DOM attributes into a React props object you can spread.
import parse, { attributesToProps } from "html-react-parser";
const options = {
replace(domNode: any) {
if (domNode.attribs && domNode.name === "main") {
const props = attributesToProps(domNode.attribs);
return <div {...props} />;
}
},
};
Transforming Every Node and Choosing Your Library
Where replace is selective, the transform option runs against every converted node. It receives the finished React node, the original DOM node, and an index, and whatever you return becomes the rendered output. It is handy for blanket wrapping or instrumentation.
parse("<br>", {
transform(reactNode) {
return <div className="wrapper">{reactNode}</div>;
},
});
The library option lets you target something other than React. Pass Preact directly, or supply a custom object exposing createElement, cloneElement, and isValidElement if you have your own rendering layer.
parse("<br>", { library: require("preact") });
Two more options round things out. The trim option removes whitespace, which is useful for elements like tables that reject stray whitespace nodes, though it can also strip intentional spacing, so reach for it deliberately. The htmlparser2 option exposes the underlying parser configuration for cases like preserving tag casing with lowerCaseTags: false. Be aware that the htmlparser2 options only take effect on the server side under Node.js and are ignored in the browser, so leaning on them can break universal rendering if you are not careful.
A Word on Security
This is the part to internalize before you ship anything. html-react-parser does not sanitize HTML and is not XSS-safe on its own. If the HTML you feed it can be influenced by users, you must clean it first. The parser also treats inline event handlers such as onclick as plain strings rather than functions, so they will not execute, and client-side <script> tags are not evaluated, but those behaviors are not a substitute for real sanitization.
The recommended pattern is to sanitize with a library like DOMPurify, and the parser gives you a clean hook for it through the trustedTypePolicy option. When running in the browser, you can pass a Trusted Types policy whose createHTML method runs your sanitizer before any content touches innerHTML.
parse("<div>Hello</div>", {
trustedTypePolicy: window.trustedTypes?.createPolicy("my-policy", {
createHTML(input) {
return DOMPurify.sanitize(input);
},
}),
});
Pairing that policy with a Content Security Policy that enforces Trusted Types gives you a solid defense without giving up the ergonomics that make the library worth using.
Versions and Server-Side Rendering
The current major release, version 6, mostly tidies the foundations: it moved the build target from es5 to es2016 and bumped its domhandler and html-dom-parser dependencies to newer majors. Upgrades across major versions have historically been gentle, leaning on dependency bumps rather than rewrites of the public API, so existing replace and transform code tends to keep working.
Server-side rendering is fully supported out of the box on Node.js, which is a large part of why the library is so popular in frameworks that prerender content. If you run into a Webpack warning about the default export not being found, the fix is to set resolve.mainFields to ["browser", "main", "module"] so the bundler picks the correct entry point.
When to Reach for It
html-react-parser is the comfortable, dependable choice whenever you have an HTML string and want controllable React output without much ceremony. It shines for rendering CMS bodies, blog content, sanitized rich text, and anything where you want to rewrite or decorate the markup on its way to the screen. If your needs are heavier, such as a full markdown pipeline built on an extensible AST, the rehype and unified ecosystem gives you more plugin power at the cost of complexity. And if all you ever do is dump a chunk of fully trusted HTML with no transformation, plain dangerouslySetInnerHTML is still the lightest option.
For everything in between, which is most real applications, this little parser hits a rare sweet spot: tiny, fast, server-friendly, and quietly powerful. It turns a string you did not write into React you fully control, and that is a trade worth making almost every time.