Rendering rich, user-generated content in React looks straightforward until you need to keep it both readable and secure. Pushing HTML strings through dangerouslySetInnerHTML can expose you to XSS attacks.
React-markdown offers a safer path by converting Markdown directly into React components—skipping that risky API entirely. You'll install the library, learn how to tailor every heading and link to your design system, add syntax-highlighted code blocks, and integrate the whole flow with Strapi so your editorial team manages content while you focus on features.
In brief:
- React-markdown converts Markdown directly to React components without using
dangerouslySetInnerHTML, eliminating XSS vulnerabilities - Custom component mapping enables seamless design system integration while maintaining proper content structure
- Security features include automatic content sanitization, element whitelisting, and URL validation
- Performance optimization techniques like memoization and code splitting keep Markdown rendering efficient at scale
What is React Markdown?
eact Markdown is a lightweight React component that renders Markdown text into HTML while maintaining React's component structure and security model. Markdown remains the fastest way to write rich content, but once you have that .md string in hand you need a React-friendly way to show it on the page.
Built on top of remark and rehype, it efficiently converts Markdown into React components without relying on dangerouslySetInnerHTML. This process not only renders Markdown safely but also ensures compliance with CommonMark standards, offering optional support for GitHub Flavored Markdown (GFM). Its popularity stems from active maintenance, a robust plugin ecosystem, and seamless compatibility with React.
While alternatives like marked and markdown-it exist, this library is particularly suited for React-first environments due to its straightforward integration and React-centric design.
Installation and Basic Setup
Getting started is straightforward. You do not need to ensure your project is running React 16.8 or later to use react-markdown, as the library does not depend on React Hooks. You can install the library using npm or yarn:
1npm install react-markdown1yarn add react-markdownOnce installed, you can import and use it in your React component:
1import ReactMarkdown from 'react-markdown';
2
3const MyComponent = () => {
4 const markdown = '# Hello, world!';
5
6 return <ReactMarkdown>{markdown}</ReactMarkdown>;
7};The Markdown content is passed as the children prop, and only a minimal dependency footprint is added to your project. Plus, TypeScript support is included by default, negating the need for additional packages.
How to Ensure Safe Markdown Rendering Without XSS Vulnerabilities
Rendering user-supplied content creates immediate security risks. If you pipe raw HTML into the DOM with dangerouslySetInnerHTML, attackers can execute JavaScript in your users' browsers—a top-ranked OWASP vulnerability.
A single Markdown link like [click](javascript:alert('XSS')) triggers an alert dialog, while an inline image such as <img src=x onerror=alert(1)> bypasses basic filters by hiding payloads in attributes. Both examples appear in XSS write-ups targeting modern front-end stacks.
The solution prevents this entirely. Instead of injecting strings, it converts each Markdown token into a React element, letting React's JSX escaping handle security—no dangerouslySetInnerHTML required. Raw HTML gets ignored, so malicious <script>alert('XSS')</script> renders as harmless text.
When you need trusted HTML, add rehype-raw and sanitize with DOMPurify.
Fine-grained controls provide additional security layers. The allowedElements and disallowedElements props create positive or negative security models:
1import ReactMarkdown from 'react-markdown';
2
3<ReactMarkdown
4 allowedElements={['p', 'strong', 'em', 'a', 'code']}
5 disallowedElements={['img', 'iframe', 'script']}
6 urlTransform={(url) => url.startsWith('https://example.com') ? url : '#blocked'}
7>
8 {markdown}
9</ReactMarkdown>This configuration permits only safe typographic tags and links, strips images and iframes, and validates URLs to stay within your domain.
Production security checklist: Validate Markdown on both client and server before rendering, sanitize HTML with DOMPurify when using trusted sources, restrict features via allowedElements/disallowedElements, filter URLs with urlTransform to block javascript: protocols, enforce strict Content Security Policy as backup defense, rate-limit submissions and monitor for attack patterns, and keep the library, plugins, and sanitizers updated—security requires ongoing maintenance.
Combining component-based output with these defenses lets you render rich content while blocking XSS attacks.
Styling Markdown Output
React-Markdown gives you complete control over visual and behavioral rendering through the components prop. Map every Markdown element (h1, a, img) to your own React components. Since rendering happens through JSX, you can inject props, context, or design-system tokens without dangerouslySetInnerHTML.
1import ReactMarkdown from 'react-markdown';
2
3function H1(props) {
4 return <h1 className="heading-xl" {...props} />;
5}
6
7function P(props) {
8 return <p className="body-md" {...props} />;
9}
10
11export default function Article({ markdown }) {
12 return (
13 <ReactMarkdown components={{ h1: H1, p: P }}>
14 {markdown}
15 </ReactMarkdown>
16 );
17}Design consistency extends beyond font sizes. You need link routing that respects your SPA, images that never overflow, and headings that pick up theme colors.
Each renderer is a React function, so wire them to any styling approach—CSS Modules, Tailwind, or styled-components. Brand colors and responsive media come directly from your design-system theme:
1import styled, { useTheme } from 'styled-components';
2
3const Img = ({ node, ...props }) => {
4 const { breakpoints } = useTheme();
5 return (
6 <figure>
7 <img
8 {...props}
9 style={{ maxWidth: '100%', borderRadius: 8 }}
10 loading="lazy"
11 />
12 {props.alt && <figcaption>{props.alt}</figcaption>}
13 <style jsx>{INLINECODE_4}</style>
14 </figure>
15 );
16};
17
18// Behavior can be customized too. Add outbound-link analytics and security headers:
19
20function Link({ href, children, ...rest }) {
21 const external = /^https?:\/\//.test(href);
22 const handleClick = () => external && window.analytics.track('outbound', { href });
23
24 return (
25 <a
26 href={href}
27 {...rest}
28 target={external ? '_blank' : undefined}
29 rel={external ? 'noopener noreferrer' : undefined}
30 onClick={handleClick}
31 >
32 {children}
33 {external && '↗'}
34 </a>
35 );
36}Build richer interactions—copy buttons for code blocks, tooltip-powered abbreviations, or callouts generated from blockquotes—all through the same components map described in the documentation.
Type safety keeps your components predictable. The library exports a Components type that pairs every node with its expected props, so your editor auto-completes attributes like node.position or inline:
1import { Components } from 'react-markdown';
2
3const H2: Components['h2'] = ({ children, ...props }) => (
4 <h2 data-level={2} {...props}>
5 {children}
6 </h2>
7);With custom renderers for style and behavior, plus strong TypeScript definitions, you render Markdown that feels indistinguishable from handcrafted JSX while preserving the safety and simplicity of plain text content.
Adding Syntax Highlighting to Code Blocks
Once you've mastered component customization, the next step is giving your code blocks the same polish the rest of your UI enjoys. Syntax highlighting handles that job, and integrating it is simpler than you might think.
Installing react-syntax-highlighter
Start by adding a dedicated highlighter library. react-syntax-highlighter supports both Prism and Highlight.js grammars, so you can pick the engine that matches your existing documentation setup.
1npm install react-syntax-highlighter prismjs
2# or
3yarn add react-syntax-highlighter prismjsAfter installation, import the Prism build with a theme:
1import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
2import { dracula } from 'react-syntax-highlighter/dist/cjs/styles/prism';That single import covers most technical documentation needs and keeps you away from the raw HTML injections that security guides warn against.
Integrating with react-markdown
The library lets you replace the default <code> renderer through the components prop. This pattern extracts the language from the class name and delegates block rendering to SyntaxHighlighter:
1import ReactMarkdown from 'react-markdown';
2import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
3import { dracula } from 'react-syntax-highlighter/dist/cjs/styles/prism';
4
5function Markdown({ source }) {
6 return (
7 <ReactMarkdown
8 components={{
9 code({ inline, className, children, ...props }) {
10 const match = /language-(\w+)/.exec(className || '');
11 if (!inline && match) {
12 return (
13 <SyntaxHighlighter
14 style={dracula}
15 language={match[1]}
16 showLineNumbers
17 wrapLongLines
18 {...props}
19 >
20 {String(children).replace(/\n$/, '')}
21 </SyntaxHighlighter>
22 );
23 }
24 return (
25 <code className={className} {...props}>
26 {children}
27 </code>
28 );
29 },
30 }}
31 >
32 {source}
33 </ReactMarkdown>
34 );
35}The match variable pulls the language (e.g., language-js) from fenced code blocks. Inline snippets stay untouched, preserving readability in your paragraphs.
The showLineNumbers and wrapLongLines props enhance longer examples, while unsupported languages fall back to plain <code> elements without breaking your markup.
Choosing and Customizing Themes
react-syntax-highlighter includes dozens of themes. Popular choices like dracula, oneDark, and atomDark provide familiar color schemes. Switching themes requires only a one-line import change, so you can respond to user preferences or system-level dark mode:
1import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
2
3// Later in component
4const isDark = useDarkMode(); // hypothetical hook
5const theme = isDark ? oneDark : dracula;For brand-level control, pass a custom style object that extends an existing theme. Colors, font sizes, and background gradients are all configurable.
Performance Considerations
Highlighting is compute-heavy, especially when importing every language by default. Keep your bundle lean and your UI responsive with targeted optimizations. Lazy-load the highlighter component with React.lazy so it only downloads when code blocks appear in your content.
Import languages selectively rather than bringing in the full Prism build, which includes hundreds of grammars you probably don't need. Memoize your code renderer with React.memo so repeated renders don't redo expensive parsing operations.
For static content like blog posts, consider pre-highlighting during your build pipeline and shipping plain HTML. This removes runtime costs entirely while maintaining the visual polish your users expect.
Combining the flexible render pipeline with a lightweight, on-demand highlighter delivers crisp, accessible code examples without bloating your bundle or sacrificing performance.
Advanced Features and Extensions
While basic rendering works well for simple documents, the remark/rehype plugin ecosystem unlocks advanced features. You can add syntax support, metadata parsing, and even diagrams without leaving React's component model.
GitHub Flavored Markdown (GFM)
If you work with GitHub issues or READMEs, you need tables, task lists, and strikethroughs. Add the remark-gfm plugin:
1npm install remark-gfm1import ReactMarkdown from 'react-markdown';
2import remarkGfm from 'remark-gfm';
3
4<ReactMarkdown remarkPlugins={[remarkGfm]}>
5 {markdown}
6</ReactMarkdown>With GFM enabled, - [x] Done renders as a checkbox, and pipes (|) create HTML tables. When tables overflow, wrap them in a scrollable container to keep layouts responsive—your design system styles apply exactly like any <table> element.
Frontmatter and Metadata
Content files often start with YAML frontmatter for titles, dates, or SEO data. Parse that section before passing the body to the renderer:
1import fs from 'fs';
2import matter from 'gray-matter';
3
4const src = fs.readFileSync('blog-post.md', 'utf8');
5const { data: meta, content } = matter(src);Now meta contains title, slug, or Strapi IDs for building routes or populating <Head> tags while content remains pure markdown. To keep YAML blocks visible to editors but hidden from readers, use the remark-frontmatter plugin to strip them during rendering. This mirrors Strapi's separation of structure (collection fields) and presentation (rich text).
Math Rendering and Diagrams
Technical blogs need inline formulas like E = mc^2 or full equations. Add remark-math for parsing and rehype-katex for client-side rendering:
1import remarkMath from 'remark-math';
2import rehypeKatex from 'rehype-katex';
3
4<ReactMarkdown
5 remarkPlugins={[remarkMath]}
6 rehypePlugins={[rehypeKatex]}
7>
8 {markdown}
9</ReactMarkdown>Complex diagrams follow the same pattern: pair remark-mermaid with a lightweight Mermaid runtime. Katex and Mermaid add significant kilobytes to your bundle, so load them lazily or gate behind feature flags when performance matters.
When to Build Custom Plugins
Existing packages solve most needs, but you might need to transform custom syntax—like <Product price="29" /> shortcodes from your marketing team. Write a remark plugin that walks the Markdown Abstract Syntax Tree (AST) and converts custom nodes to shapes your React components understand.
The remark documentation explains the visitor pattern with examples. Search the plugin registry first—someone else likely solved your problem already, saving bundle size and maintenance work.
Performance and Production Best Practices
Rendering Markdown triggers a full parse on every re-render. As your documents grow and you add custom components or syntax highlighting, that parsing cost becomes noticeable. Here's how to keep your rendering fast without losing functionality.
Memoization and Re-render Prevention
The library builds an abstract syntax tree from raw text on each render, so preventing unnecessary cycles saves significant processing time. Wrap your renderer in React.memo and memoize the components map with useMemo. React won't re-parse identical input, but will still update when your content or styling changes.
1import React, { useMemo } from 'react';
2import ReactMarkdown from 'react-markdown';
3
4const Markdown = React.memo(function Markdown({ source, components }) {
5 const memoizedComponents = useMemo(() => components, [components]);
6
7 return <ReactMarkdown components={memoizedComponents}>{source}</ReactMarkdown>;
8});Handling Large Documents
Long posts or knowledge-base articles can overwhelm the DOM and slow down user devices. Split your content into logical sections, then lazy-load or virtualize what isn't visible. A windowing library like react-window renders only the visible lines, keeping memory usage manageable.
1import { FixedSizeList as List } from 'react-window';
2import ReactMarkdown from 'react-markdown';
3
4const lines = markdown.split('\n');
5
6<List height={600} itemCount={lines.length} itemSize={24} width="100%">
7 {({ index, style }) => (
8 <div style={style}>
9 <ReactMarkdown>{lines[index]}</ReactMarkdown>
10 </div>
11 )}
12</List>When you can use static generation, pre-render pages at build time so the browser handles zero parsing work on first paint. Parsing huge files at runtime causes noticeable delays for your users.
Loading States and Error Handling
Markdown often comes from an API or headless CMS. Treat it like any remote resource: show a spinner while fetching, catch network errors gracefully, and sanitize before rendering.
1function Post({ id }) {
2 const { data, error, isLoading } = useFetch(`/api/posts/${id}`);
3
4 if (isLoading) return <Spinner />;
5 if (error) return <ErrorMessage />;
6
7 return <Markdown source={DOMPurify.sanitize(data.body)} />;
8}Wrap your component in an error boundary to protect the rest of your UI if a plugin throws on malformed input.
Content Security Policy and Bundle Size
A strict Content-Security-Policy header—default-src 'self'; script-src 'none'—adds browser-level protection against XSS, complementing the sanitization techniques recommended by security researchers.
Your performance directly connects to security choices. Each plugin, syntax-highlighting theme, and additional language grammar inflates your bundle. Code split with React.lazy so syntax highlighters load only on pages that need them. Use "light" builds by importing specific languages instead of the full registry. Consider CDN delivery for heavy, rarely changing assets to shift bandwidth off your origin.
Combine these practices and you get a renderer that stays secure, predictable, and fast—no matter how much content you're processing.
Real-World Production Scenarios
The techniques from previous sections work best when applied to real applications. These four scenarios show you how to combine security, customization, and performance patterns for production use.
Blog Content from Strapi CMS
If you configure Strapi to store rich posts as Markdown fields, and fetch that content into your React front-end, security comes first. The react-markdown library skips dangerouslySetInnerHTML by default, but you still need to sanitize any embedded HTML or malicious links authors might include. DOMPurify closes the most common XSS vulnerabilities security researchers identify.
1import DOMPurify from 'dompurify';
2import ReactMarkdown from 'react-markdown';
3
4export default function BlogPost({ markdown }) {
5 const safe = DOMPurify.sanitize(markdown); // XSS protection
6 return <ReactMarkdown>{safe}</ReactMarkdown>;
7}Blog articles mix headings, images, and code snippets, so use the components prop to apply your design system. A custom img renderer adds lazy-loading, while a code renderer integrates syntax highlighting without breaking performance.
Wrap your entire post component in React.memo to avoid re-parsing when users navigate between pages.
User-Generated Content (Comments, Forums, Posts)
Wherever strangers can type Markdown, you have an attack surface. The library ignores raw HTML by default, but links like [x](javascript:alert(1)) remain dangerous. DOMPurify scrubs these protocols, and you should validate content both client- and server-side.
For better user experience, feed the same sanitization function into a live preview pane so users see exactly what will be saved. You can enforce character limits and link whitelists with allowedElements and a custom URL transformer.
Documentation Sites and Knowledge Bases
Large docs bog down the browser if every page renders in a single pass. Parsing oversized files hurts time-to-interactive.
Two optimizations fix this. First, memoize the renderer so repeated navigation doesn't trigger fresh parses:
1const Markdown = React.memo(({ source }) => <ReactMarkdown>{source}</ReactMarkdown>);Second, split long documents into smaller sections and render them on demand using virtualization strategies. You can extract heading data while parsing to feed a sidebar table-of-contents component. Since you're working with React elements, no unsafe HTML touches the DOM.
Embedding Interactive React Components
Plain Markdown isn't enough when you need live calculators, product cards, or interactive widgets inside article bodies. Enable the rehype-raw plugin and map custom tags to React components:
1import rehypeRaw from 'rehype-raw';
2import ReactMarkdown from 'react-markdown';
3
4<ReactMarkdown
5 rehypePlugins={[rehypeRaw]}
6 components={{
7 product: ({node, ...props}) => <Product {...props} />
8 }}
9>
10 {safeMarkdown}
11</ReactMarkdown>This approach keeps your JSX in control while authors drop simple <product /> tags in their content.
Raw HTML opens the door to XSS if you skip sanitization. Pair rehype-raw with DOMPurify and a strict Content Security Policy, as security experts recommend.
By combining the composable API with proven security and performance techniques, you can safely render everything from Strapi-powered blogs to community forums and interactive documentation.
Combine React Markdown With Strapi for Optimal Results
When you pipe Markdown from Strapi into react-markdown, you avoid dangerouslySetInnerHTML, get automatic XSS protection, and control every rendered element through the components prop.
The result is secure content that renders as native React elements—no HTML string sanitization required. Combining Strapi's API-driven CMS with this React renderer lets you iterate on content and presentation independently, speeding up releases and removing the typical friction between content authors and developers.