How to Render Markdown from a React Component with react-markdown
Use the react-markdown library to parse Markdown strings into native React elements via the remark ecosystem, eliminating the need for dangerouslySetInnerHTML while supporting custom renderers, syntax highlighting, and server-side rendering.
To render markdown from a React component safely and efficiently, the open-source react-markdown library (maintained in the remarkjs/react-markdown repository) processes content through a unified syntax tree pipeline. Unlike solutions that inject raw HTML strings directly into the DOM, this approach constructs a React element tree from the Markdown Abstract Syntax Tree (MDAST), ensuring full compatibility with React’s virtual DOM lifecycle, hooks, and server-side rendering contexts.
How react‑markdown Works Under the Hood
The library’s architecture in src/ReactMarkdown.tsx avoids string-based HTML manipulation by transforming content through three distinct phases before reaching the browser.
Phase 1: Parsing with remark
At its core, react-markdown feeds the input string into remark-parse to generate an MDAST (Markdown Abstract Syntax Tree). This structured representation captures every semantic node—headings, paragraphs, lists, and code blocks—as discrete objects with positional metadata, enabling precise manipulation before any visual rendering occurs.
Phase 2: Rehype Transformation to HAST
When the rehypePlugins option is provided (configured in src/rehypePlugins.ts), the library converts the MDAST into an HAST (HTML Abstract Syntax Tree). This transformation step allows plugins to modify HTML-bound structures—such as adding IDs to headings for anchor links—while remaining in the abstract tree domain rather than dealing with fragile string concatenation.
Phase 3: React Component Mapping
Finally, the library walks the syntax tree and maps each node type to a corresponding React component. The default renderers in src/components/*.tsx generate standard elements like <h1> or <p>, but you can override any node type via the components prop to inject custom behavior, styling, or interactive widgets without escaping React’s component model.
Basic Implementation
Install the package via your package manager:
npm install react-markdown
Then import the main component and pass your Markdown string as children:
import React from 'react';
import ReactMarkdown from 'react-markdown';
function SimpleMarkdown() {
const markdown = `
# Hello World
This is **bold** and this is *italic*.
- Item 1
- Item 2
[Visit React](https://reactjs.org)
`;
return <ReactMarkdown>{markdown}</ReactMarkdown>;
}
This renders a native React element tree containing an <h1>, styled text, a <ul> list, and an <a> link—no dangerouslySetInnerHTML required.
Customizing Renderers with the components Prop
To override how specific nodes render, pass an object to the components prop mapping node names to custom React components. This is particularly powerful for integrating syntax highlighters.
import React from 'react';
import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
const markdown = `
## Code Sample
\`\`\`js
function greet(name) {
return \`Hello, \${name}!\`;
}
\`\`\`
`;
function MarkdownWithCode() {
return (
<ReactMarkdown
components={{
code({node, inline, className, children, ...props}) {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<SyntaxHighlighter language={match[1]} PreTag="div" {...props}>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className={className} {...props}>
{children}
</code>
);
},
}}
>
{markdown}
</ReactMarkdown>
);
}
Here, the custom code renderer intercepts fenced code blocks detected by the /language-(\w+)/ regex and swaps them for SyntaxHighlighter components, while inline code backticks render through the standard <code> element.
Handling HTML Content Securely
By default, react-markdown treats security as opt-out rather than opt-in: the allowedElements list is empty, meaning raw HTML tags in your Markdown source are discarded rather than injected into the DOM. To enable HTML rendering, you must explicitly set skipHtml={false} and sanitize the output to prevent XSS vulnerabilities.
import React from 'react';
import ReactMarkdown from 'react-markdown';
import DOMPurify from 'dompurify';
const markdown = `
<div class="alert">⚠️ This is **HTML** inside markdown.</div>
`;
function MarkdownWithHtml() {
return (
<ReactMarkdown
skipHtml={false}
transformHtml={(html) => DOMPurify.sanitize(html)}
>
{markdown}
</ReactMarkdown>
);
}
The transformHtml hook (available from v8 onward) receives the raw HTML string before it reaches the DOM, allowing you to run DOMPurify or similar sanitizers to strip malicious scripts while preserving semantic markup.
Server-Side Rendering Support
Because react-markdown operates entirely within React’s rendering cycle—building elements rather than concatenating strings—it integrates seamlessly with server-side frameworks like Next.js. The component works inside getStaticProps or getServerSideProps without window-dependent APIs.
// pages/post/[slug].tsx
import ReactMarkdown from 'react-markdown';
import { GetStaticProps } from 'next';
export const getStaticProps: GetStaticProps = async ({ params }) => {
const markdown = await fetchMarkdownFromCMS(params?.slug as string);
return { props: { markdown } };
};
export default function Post({ markdown }: { markdown: string }) {
return <ReactMarkdown>{markdown}</ReactMarkdown>;
}
This static generation pattern ensures your Markdown content is pre-rendered to HTML on the server, hydrating into interactive React components on the client without hydration mismatches.
Summary
- Abstract Syntax Tree pipeline:
src/ReactMarkdown.tsxleverages remark for MDAST generation and optional rehype plugins for HAST transformation, keeping processing out of the browser’s DOM until the final React element creation step. - Component-level customization: The
componentsprop insrc/components/*.tsxallows granular overrides of every Markdown node type, enabling syntax highlighting and design system integration without escaping React. - Security-first defaults: Raw HTML is blocked unless you explicitly disable
skipHtmland implement sanitization viatransformHtml, protecting against XSS attacks inherent indangerouslySetInnerHTMLapproaches. - SSR and SSG compatible: The library’s pure-JavaScript AST processing works in Node.js environments, supporting Next.js static site generation and traditional server-side rendering without environment guards.
Frequently Asked Questions
What is the difference between react-markdown and dangerouslySetInnerHTML?
react-markdown constructs a React element tree through src/ReactMarkdown.tsx, preserving React’s virtual DOM diffing, event delegation, and lifecycle methods. In contrast, dangerouslySetInnerHTML injects raw HTML strings directly into the DOM, bypassing React’s sanitization and reconciliation algorithms, which exposes applications to XSS risks and breaks component encapsulation.
How do I enable syntax highlighting in code blocks?
Pass a custom renderer to the components prop that detects the language-* class name on code nodes (as parsed by src/remarkPlugins.ts), then return a syntax highlighter component like react-syntax-highlighter for non-inline blocks while returning a standard <code> element for inline text.
Can I use react-markdown with Next.js static site generation?
Yes. Because the library’s processing in src/rehypePlugins.ts and src/ReactMarkdown.tsx relies only on JavaScript AST transformations without window or document dependencies, it executes safely inside getStaticProps or getServerSideProps, enabling static HTML generation at build time that hydrates correctly on the client.
Why does HTML in my Markdown not render by default?
The library’s security architecture defaults skipHtml to true and initializes allowedElements as an empty array in the internal options parsing. This design prevents accidental XSS vulnerabilities from user-generated content. To render HTML elements, you must explicitly set skipHtml={false} and provide a transformHtml sanitizer (such as DOMPurify) to scrub malicious attributes before they reach the DOM.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s https://instagit.com/install.md Maintain an open-source project? Get it listed too →