html-react-parser: the second callback argument is not a key, the CDN example pins React 18, and the UMD build fails on warnings
📝 HTML to React parser.
At a glance
- What is it?
- A small library that turns an HTML string into React elements, on the server and in the browser, with options for replacing and transforming elements. It is the kind of dependency people add in five minutes and then debug for an afternoon, and the documentation is unusually honest about where that happens, because the FAQ is written almost entirely out of failure reports.
- Who is it for?
- Use it when you already hold HTML and need React elements, and not when you need a sanitiser, because that is a different job with a different answer. Two things to read before you adopt it.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The second callback argument restarts at zero for every node
The most useful warning on the page is a single note attached to the replace callback's second parameter.
The callback receives the node and, as its second argument, the index of that node. The note says the index restarts at zero when the traversal moves into a node's children, and that you should not rely on it being a unique key. There is a link to the issue where this was worked out.
That is a small sentence with a large blast radius, because index is exactly what a React developer reaches for when a list needs a key. A replace callback that returns an element per node and uses the index as the key will collide the moment the tree nests, and the symptom is a console warning rather than a crash, which is the worst kind of bug to notice late. The alternative is on the page too: there is a transform option, and the replace callback returns a real element rather than an index.
It is worth saying plainly that this is the library documenting a sharp edge in its own API rather than a user report. For a package whose whole job is walking a tree and handing elements to React, the numbering scheme is the kind of thing that has to be stated, and stating it costs one paragraph.
The replace callback gets a parser node, not a React element
The callback receives a node from an HTML parsing library, and the page makes you look at its shape before using it.
Element {
type: 'tag',
parent: null,
prev: null,
next: null,
startIndex: null,
endIndex: null,
children: [],
name: 'br',
attribs: {}
}So it is a tree node with a type tag, a name, a sibling chain, source positions, an attribute bag, and children, and it arrives before any React element exists. The replacement rule is one sentence: the element is replaced if a valid React element is returned, which means returning nothing leaves the node alone and returning a transformed version of its children requires a second exported function.
That second function is the interesting part for anyone writing a recursive rewrite, and the page demonstrates exactly that shape: match on the node's attributes, and when they match, return a new element whose contents are produced by handing the node's children back to the same function with the same options. One function, applied to a subtree, rewriting as it descends.
For TypeScript users there are two documented ways in, and both are shown: check that the node is an instance of the exported element class before reading its attribute bag, or cast it. The page also carries a troubleshooting entry for the exact error you get when you read attributes without doing either.
The CDN example pins React to an old major and floats the library
There are three documented install paths, and the one aimed at people without a bundler is the one to read carefully.
<!-- HTMLReactParser depends on React -->
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/html-react-parser@latest/dist/html-react-parser.min.js"></script>The comment above it is the honest part: the library depends on React, so the page loads React first. Then the pins. React is pinned to a major version, and the library itself is loaded from a floating latest tag. That is the inverse of the usual arrangement, where the dependency floats and the thing you are shipping is pinned, and it means two scripts loaded six months apart will behave differently.
The bundle it points at is the universal build, built by the project's own bundler, and the global it defines is the parser function itself, ready to call with a string. So the script path is genuinely zero-build, at the cost of a stale peer and an unpinned library.
The package-manager paths have no such problem, which is the other half of the answer: one command with the package saved as a dependency, one with the package added, and both pin by lockfile as soon as your project has one. The script tag is for a quick page, and it is written like one.
Requiring the package means reaching for a default export
Two import styles are documented side by side, and the difference is one word.
const parse = require('html-react-parser').default;The ES module import is the ordinary one. The CommonJS require, however, does not return the function directly; you take the default property off it. That is the interop tax you pay when a package is published as an ES module with a CommonJS build alongside, which the manifest confirms by pointing its main field at one build, its module field at another, and declaring conditional exports for both plus a deep path that maps either build.
That export map is doing more work than usual. Beyond the root entry there is a wildcard subpath, so consumers can reach into the internal file layout of either build, in module or script form. It is the kind of thing that makes a bundler configuration easier and a major version bump riskier, since anything importing a deep path is depending on the file layout rather than the public surface.
One more constraint from the examples is worth knowing before you paste anything in: adjacent parsed elements have to be rendered under a parent. Parsing two list items and rendering them side by side is invalid, so the page wraps them in an unordered list, and the parse happens inside that element. That is a React rule rather than a library one, but it is the first error a newcomer hits.
The universal build fails on any warning
Two build commands produce the two published shapes, and the second one carries a flag that says something about the project's tolerance for noise.
"build:umd": "rollup --config --failAfterWarnings",The commonjs build is the TypeScript compiler with a build configuration. The universal build is the bundler, and it is configured to fail after warnings, meaning a warning is an error. For a library whose entire public surface is one function, that is a cheap way to keep the bundle honest: any dependency that starts complaining, any circular reference, any unused export warning, stops the release rather than shipping.
The publish path is stricter still. The prepublish hook runs the build, then the linter, then the type checker against the test configuration, then a package validator, then the test suite. Four checks in sequence before anything leaves the machine, with the package validator checking that the manifest's entry points and file lists match what is actually on disk.
Supporting that is a repository organised around the same concerns. There is a size budget file, so the published weight is a checked number rather than an accident. There are two test configurations, one for the standard runner and one that runs the suite in a real browser, so the browser path is tested rather than assumed. There is a separate end-to-end script that builds first and then runs the integration folder that the default test command excludes. And there are three separate TypeScript configurations for building, for testing, and for the editor.
The FAQ is written from failure reports
The table of contents is the most informative part of this readme, because it is organised by symptom rather than by feature.
There are twelve questions in it and nearly all of them are things going wrong. Is it safe against script injection. Does invalid input get sanitised. Are script tags parsed at all. Why attributes are not being called. Why the parser throws. Whether server-side rendering works. Why elements are not nested correctly. Why tag case changes. A specific type error about reading attributes off a node. Whether whitespace trimming can be enabled for only some elements. Build warnings from one specific bundler. A type error with no more detail than that.
That is a support history rendered as documentation, and it is a better guide than a feature list would be, because each entry corresponds to a question somebody actually asked. It also tells you where the edges are. Script tags are a subject, which means the parser has an opinion about them. Sanitising is a subject, which means the maintainer considers it a question this library should not be answering for you. Whitespace trimming is per element, which means the default is global and the escape hatch is an allowlist.
Above the FAQ sits a migration section with five entries, one for each of the last five major versions. For a library at version six, five migration notes is the record of five times something changed shape, and it is the section to read if you are upgrading rather than installing.
Editorial conclusion
Use it when you already hold HTML and need React elements, and not when you need a sanitiser, because that is a different job with a different answer. Two things to read before you adopt it. The replace callback's second argument restarts at zero for each node's children, so anything that depends on it being unique will produce duplicate keys, and the page links the issue where that was worked out. And its published type declarations are checked by a third-party packer on every publish, which is a small signal about the care behind a package this widely used.
Frequently asked questions
what is html react parser
A library that converts an HTML string into one or more React elements, and it works both on the server and in the browser. Its options cover replacing elements, transforming them, choosing the underlying HTML parser, trimming whitespace, and supplying a trusted type policy.
is html react parser safe
The page treats this as a documented question rather than a claim, listing script injection, whether invalid input gets sanitised, and whether script tags are parsed as three separate entries in its FAQ. It also documents replacing an element with another one, which is how you strip an element you do not want rendered.
How do I install html-react-parser?
Three documented routes: an install command through the package registry with the package saved, an add command through a different package manager, or a script tag from a content delivery network, which loads React first because the library depends on it and exposes a global function.
Why do my keys collide when I use the replace option?
The second argument the callback receives is an index that restarts at zero each time the traversal enters a node's children, so it is not unique across a tree. The page links the issue where that behaviour was worked out.
Does html-react-parser check that my types are correct before publishing?
Yes. The publish hook runs the build, the linter, the type checker, a package validator, and the test suite in sequence, and one of those steps packs the project and runs a third-party tool that verifies the published type declarations resolve correctly.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/remarkablemark-html-react-parser)