React-markdown is a popular React component library that allows you to render markdown content in your React applications. This comprehensive guide will teach you everything you need to know about using react-markdown.
React-markdown is a markdown renderer for React that converts markdown text into React components. It's built on top of the remark markdown parser and provides a safe, extensible way to render markdown in React applications.
Install react-markdown using npm or yarn:
npm install react-markdown
yarn add react-markdown
The simplest way to use react-markdown is to import the component and pass markdown text as children:
import React from 'react';
import ReactMarkdown from 'react-markdown';
function App() {
const markdown = `# Hello World
This is **bold** text and this is *italic* text.
- List item 1
- List item 2
- List item 3
`;
return (
<ReactMarkdown>{markdown}</ReactMarkdown>
);
}
export default App;
You can also pass markdown content using the children prop:
import ReactMarkdown from 'react-markdown';
function MarkdownRenderer({ content }) {
return (
<ReactMarkdown children={content} />
);
}
One of the powerful features of react-markdown is the ability to customize how markdown elements are rendered:
import ReactMarkdown from 'react-markdown';
const components = {
h1: ({node, ...props}) => <h1 style={{color: 'blue'}} {...props} />,
p: ({node, ...props}) => <p style={{fontSize: '18px'}} {...props} />,
code: ({node, inline, ...props}) =>
inline
? <code style={{background: '#f4f4f4'}} {...props} />
: <pre><code {...props} /></pre>,
};
function CustomMarkdown({ content }) {
return (
<ReactMarkdown components={components}>
{content}
</ReactMarkdown>
);
}
Here are some commonly used props in react-markdown:
React-markdown supports plugins to extend functionality. Here's an example with syntax highlighting:
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
function MarkdownWithSyntaxHighlighting({ content }) {
return (
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
code({node, inline, className, children, ...props}) {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<SyntaxHighlighter
style={vscDarkPlus}
language={match[1]}
PreTag="div"
{...props}
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className={className} {...props}>
{children}
</code>
);
}
}}
>
{content}
</ReactMarkdown>
);
}
To support GitHub Flavored Markdown (GFM) features like tables, strikethrough, and task lists, use the remark-gfm plugin:
npm install remark-gfm
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
function GFMExample({ content }) {
return (
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{content}
</ReactMarkdown>
);
}
React-markdown is safe by default and protects against XSS attacks. However, if you're allowing user-generated content, consider:
skipHtml prop to disable HTML renderingallowedElements to whitelist specific elementsdisallowedElements to blacklist dangerous elementsFor better performance with react-markdown:
React-markdown has excellent TypeScript support. Install types if needed:
import ReactMarkdown from 'react-markdown';
import { Components } from 'react-markdown';
interface MarkdownProps {
content: string;
}
const components: Components = {
h1: ({children}) => <h1 className="title">{children}</h1>,
p: ({children}) => <p className="paragraph">{children}</p>,
};
function TypedMarkdown({ content }: MarkdownProps) {
return (
<ReactMarkdown components={components}>
{content}
</ReactMarkdown>
);
}
Create custom link components with additional functionality:
import ReactMarkdown from 'react-markdown';
const components = {
a: ({node, href, children, ...props}) => (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="custom-link"
{...props}
>
{children}
</a>
),
};
function CustomLinks({ content }) {
return (
<ReactMarkdown components={components}>
{content}
</ReactMarkdown>
);
}
Add lazy loading, error handling, or other features to images:
const components = {
img: ({node, src, alt, ...props}) => (
<img
src={src}
alt={alt}
loading="lazy"
onError={(e) => {
e.target.src = '/placeholder.png';
}}
{...props}
/>
),
};
Handle errors gracefully when rendering markdown:
import ReactMarkdown from 'react-markdown';
import { useState } from 'react';
function SafeMarkdownRenderer({ content }) {
const [error, setError] = useState(null);
try {
return (
<ReactMarkdown>
{content}
</ReactMarkdown>
);
} catch (err) {
return (
<div className="error">
Error rendering markdown: {err.message}
</div>
);
}
}
Integrate react-markdown with state management libraries:
import ReactMarkdown from 'react-markdown';
import { useSelector } from 'react-redux';
function MarkdownFromStore() {
const markdownContent = useSelector(state => state.content.markdown);
return (
<ReactMarkdown>
{markdownContent}
</ReactMarkdown>
);
}
Use react-markdown with Next.js or other SSR frameworks:
import ReactMarkdown from 'react-markdown';
import { readFileSync } from 'fs';
export async function getStaticProps() {
const content = readFileSync('content.md', 'utf-8');
return { props: { content } };
}
export default function Page({ content }) {
return (
<ReactMarkdown>
{content}
</ReactMarkdown>
);
}
Test your react-markdown components effectively:
import { render, screen } from '@testing-library/react';
import ReactMarkdown from 'react-markdown';
test('renders markdown correctly', () => {
const markdown = '# Hello World\n\nThis is a test.';
render(<ReactMarkdown>{markdown}</ReactMarkdown>);
expect(screen.getByText('Hello World')).toBeInTheDocument();
expect(screen.getByText('This is a test.')).toBeInTheDocument();
});
Convert line breaks to `
` tags:
import ReactMarkdown from 'react-markdown';
import remarkBreaks from 'remark-breaks';
<ReactMarkdown remarkPlugins={[remarkBreaks]}>
{content}
</ReactMarkdown>
Render mathematical equations:
import ReactMarkdown from 'react-markdown';
import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';
import 'katex/dist/katex.min.css';
<ReactMarkdown
remarkPlugins={[remarkMath]}
rehypePlugins={[rehypeKatex]}
>
{content}
</ReactMarkdown>
If markdown isn't rendering:
If styles aren't applying:
If rendering is slow:
If migrating from marked library:
Key takeaways for using react-markdown effectively:
For more information about react-markdown:
Create styled blockquotes with icons or special formatting:
const components = {
blockquote: ({node, children, ...props}) => (
<blockquote className="custom-blockquote" {...props}>
<div className="quote-icon">❝</div>
<div className="quote-content">{children}</div>
</blockquote>
),
};
Enhance tables with sorting, filtering, or responsive design:
const components = {
table: ({node, children, ...props}) => (
<div className="table-wrapper">
<table className="markdown-table" {...props}>
{children}
</table>
</div>
),
thead: ({node, children, ...props}) => (
<thead className="table-header" {...props}>
{children}
</thead>
),
tbody: ({node, children, ...props}) => (
<tbody className="table-body" {...props}>
{children}
</tbody>
),
tr: ({node, children, ...props}) => (
<tr className="table-row" {...props}>
{children}
</tr>
),
th: ({node, children, ...props}) => (
<th className="table-head" {...props}>
{children}
</th>
),
td: ({node, children, ...props}) => (
<td className="table-cell" {...props}>
{children}
</td>
),
};
Style lists with custom icons or animations:
const components = {
ul: ({node, children, ...props}) => (
<ul className="custom-list" {...props}>
{children}
</ul>
),
ol: ({node, children, ...props}) => (
<ol className="custom-ordered-list" {...props}>
{children}
</ol>
),
li: ({node, children, ...props}) => (
<li className="custom-list-item" {...props}>
<span className="list-marker"></span>
{children}
</li>
),
};
Add IDs to headings automatically for anchor links:
npm install remark-slug
import ReactMarkdown from 'react-markdown';
import remarkSlug from 'remark-slug';
<ReactMarkdown remarkPlugins={[remarkSlug]}>
{content}
</ReactMarkdown>
Automatically add links to headings:
npm install remark-autolink-headings
import ReactMarkdown from 'react-markdown';
import remarkSlug from 'remark-slug';
import remarkAutolinkHeadings from 'remark-autolink-headings';
<ReactMarkdown
remarkPlugins={[
remarkSlug,
[remarkAutolinkHeadings, { behavior: 'wrap' }]
]}
>
{content}
</ReactMarkdown>
Convert emoji shortcodes to emoji:
npm install remark-emoji
import ReactMarkdown from 'react-markdown';
import remarkEmoji from 'remark-emoji';
<ReactMarkdown remarkPlugins={[remarkEmoji]}>
{content}
</ReactMarkdown>
Support footnotes in markdown:
npm install remark-footnotes
import ReactMarkdown from 'react-markdown';
import remarkFootnotes from 'remark-footnotes';
<ReactMarkdown remarkPlugins={[remarkFootnotes]}>
{content}
</ReactMarkdown>
Enhance image handling with size attributes:
npm install remark-images
import ReactMarkdown from 'react-markdown';
import remarkImages from 'remark-images';
<ReactMarkdown remarkPlugins={[remarkImages]}>
{content}
</ReactMarkdown>
Parse HTML in markdown (use with caution):
npm install rehype-raw
import ReactMarkdown from 'react-markdown';
import rehypeRaw from 'rehype-raw';
<ReactMarkdown rehypePlugins={[rehypeRaw]}>
{content}
</ReactMarkdown>
Sanitize HTML to prevent XSS attacks:
npm install rehype-sanitize
import ReactMarkdown from 'react-markdown';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize from 'rehype-sanitize';
<ReactMarkdown
rehypePlugins={[rehypeRaw, rehypeSanitize]}
>
{content}
</ReactMarkdown>
Syntax highlighting for code blocks:
npm install rehype-highlight
import ReactMarkdown from 'react-markdown';
import rehypeHighlight from 'rehype-highlight';
import 'highlight.js/styles/github.css';
<ReactMarkdown rehypePlugins={[rehypeHighlight]}>
{content}
</ReactMarkdown>
Add slug IDs to headings (rehype version):
npm install rehype-slug
import ReactMarkdown from 'react-markdown';
import rehypeSlug from 'rehype-slug';
<ReactMarkdown rehypePlugins={[rehypeSlug]}>
{content}
</ReactMarkdown>
Complete blog post component with metadata and styling:
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import remarkSlug from 'remark-slug';
import remarkAutolinkHeadings from 'remark-autolink-headings';
function BlogPost({ content, title, date, author }) {
const components = {
h1: ({node, ...props}) => (
<h1 className="blog-title" {...props} />
),
h2: ({node, ...props}) => (
<h2 className="blog-heading" {...props} />
),
img: ({node, src, alt, ...props}) => (
<img
src={src}
alt={alt}
className="blog-image"
loading="lazy"
{...props}
/>
),
a: ({node, href, children, ...props}) => (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="blog-link"
{...props}
>
{children}
</a>
),
};
return (
<article className="blog-post">
<header className="blog-header">
<h1>{title}</h1>
<div className="blog-meta">
<span>By {author}</span>
<span>{date}</span>
</div>
</header>
<ReactMarkdown
remarkPlugins={[
remarkGfm,
remarkSlug,
[remarkAutolinkHeadings, { behavior: 'wrap' }]
]}
components={components}
>
{content}
</ReactMarkdown>
</article>
);
}
Documentation renderer with table of contents:
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import remarkSlug from 'remark-slug';
import { useState, useEffect } from 'react';
function Documentation({ content }) {
const [headings, setHeadings] = useState([]);
useEffect(() => {
const headingElements = document.querySelectorAll('h2, h3');
const headingData = Array.from(headingElements).map(el => ({
id: el.id,
text: el.textContent,
level: el.tagName.toLowerCase(),
}));
setHeadings(headingData);
}, [content]);
return (
<div className="documentation-container">
<aside className="table-of-contents">
<h3>Table of Contents</h3>
<ul>
{headings.map(heading => (
<li key={heading.id} className={`toc-${heading.level}`}>
<a href={`#${heading.id}`}>{heading.text}</a>
</li>
))}
</ul>
</aside>
<main className="documentation-content">
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkSlug]}
>
{content}
</ReactMarkdown>
</main>
</div>
);
}
User comment renderer with sanitization:
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeSanitize from 'rehype-sanitize';
function CommentRenderer({ content, author, timestamp }) {
const components = {
// Disable images in comments for security
img: () => null,
// Limit link functionality
a: ({node, href, children, ...props}) => {
const isExternal = href?.startsWith('http');
return (
<a
href={href}
target={isExternal ? '_blank' : undefined}
rel={isExternal ? 'noopener noreferrer' : undefined}
className="comment-link"
{...props}
>
{children}
</a>
);
},
};
return (
<div className="comment">
<div className="comment-header">
<strong>{author}</strong>
<time>{timestamp}</time>
</div>
<div className="comment-body">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeSanitize]}
components={components}
allowedElements={['p', 'strong', 'em', 'code', 'a', 'ul', 'ol', 'li']}
>
{content}
</ReactMarkdown>
</div>
</div>
);
}
Live markdown editor with split view:
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { useState } from 'react';
function MarkdownEditor() {
const [markdown, setMarkdown] = useState('# Start writing...\n\nYour markdown here.');
return (
<div className="markdown-editor">
<div className="editor-pane">
<textarea
value={markdown}
onChange={(e) => setMarkdown(e.target.value)}
className="markdown-input"
placeholder="Write your markdown here..."
/>
</div>
<div className="preview-pane">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{markdown}
</ReactMarkdown>
</div>
</div>
);
}
Memoize components and content to prevent unnecessary re-renders:
import ReactMarkdown from 'react-markdown';
import { useMemo, memo } from 'react';
const MemoizedMarkdown = memo(ReactMarkdown);
function OptimizedRenderer({ content }) {
const memoizedContent = useMemo(() => content, [content]);
return (
<MemoizedMarkdown>
{memoizedContent}
</MemoizedMarkdown>
);
}
Lazy load react-markdown for better initial load time:
import { lazy, Suspense } from 'react';
const ReactMarkdown = lazy(() => import('react-markdown'));
function LazyMarkdown({ content }) {
return (
<Suspense fallback={<div>Loading...</div>}>
<ReactMarkdown>{content}</ReactMarkdown>
</Suspense>
);
}
For very long documents, consider virtual scrolling:
import ReactMarkdown from 'react-markdown';
import { FixedSizeList } from 'react-window';
function VirtualizedMarkdown({ content }) {
// Split content into sections
const sections = content.split('\n\n');
return (
<FixedSizeList
height={600}
itemCount={sections.length}
itemSize={100}
>
{({ index, style }) => (
<div style={style}>
<ReactMarkdown>{sections[index]}</ReactMarkdown>
</div>
)}
</FixedSizeList>
);
}
Add proper ARIA labels to custom components:
const components = {
img: ({node, src, alt, ...props}) => (
<img
src={src}
alt={alt || 'Image'}
role="img"
aria-label={alt || 'Image'}
{...props}
/>
),
a: ({node, href, children, ...props}) => (
<a
href={href}
aria-label={`Link to ${children}`}
{...props}
>
{children}
</a>
),
};
Ensure custom interactive components support keyboard navigation:
const components = {
a: ({node, href, children, ...props}) => (
<a
href={href}
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
window.location.href = href;
}
}}
{...props}
>
{children}
</a>
),
};
Configure remark and rehype parsers:
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { remark } from 'remark';
import { rehype } from 'rehype';
const processor = remark()
.use(remarkGfm)
.use(/* custom plugins */);
<ReactMarkdown
remarkPlugins={[remarkGfm]}
remarkRehypeOptions={{
allowDangerousHtml: false,
clobberPrefix: 'user-content-',
}}
>
{content}
</ReactMarkdown>
Conditionally render different components based on content:
import ReactMarkdown from 'react-markdown';
function ConditionalRenderer({ content, allowImages = true }) {
const components = {
img: allowImages
? ({node, ...props}) => <img {...props} />
: () => null,
};
return (
<ReactMarkdown components={components}>
{content}
</ReactMarkdown>
);
}
Render markdown content from route parameters:
import ReactMarkdown from 'react-markdown';
import { useParams } from 'react-router-dom';
import { useEffect, useState } from 'react';
function MarkdownPage() {
const { slug } = useParams();
const [content, setContent] = useState('');
useEffect(() => {
fetch(`/content/${slug}.md`)
.then(res => res.text())
.then(text => setContent(text));
}, [slug]);
return (
<ReactMarkdown>{content}</ReactMarkdown>
);
}
Fetch and render markdown from GraphQL API:
import ReactMarkdown from 'react-markdown';
import { useQuery } from '@apollo/client';
import { gql } from '@apollo/client';
const GET_POST = gql`
query GetPost($id: ID!) {
post(id: $id) {
content
title
}
}
`;
function PostPage({ postId }) {
const { data, loading } = useQuery(GET_POST, {
variables: { id: postId }
});
if (loading) return <div>Loading...</div>;
return (
<div>
<h1>{data.post.title}</h1>
<ReactMarkdown>{data.post.content}</ReactMarkdown>
</div>
);
}
Render markdown from Contentful:
import ReactMarkdown from 'react-markdown';
import { useEffect, useState } from 'react';
import * as contentful from 'contentful';
const client = contentful.createClient({
space: 'your-space-id',
accessToken: 'your-access-token'
});
function ContentfulPost({ entryId }) {
const [content, setContent] = useState('');
useEffect(() => {
client.getEntry(entryId)
.then(entry => setContent(entry.fields.body));
}, [entryId]);
return (
<ReactMarkdown>{content}</ReactMarkdown>
);
}
Add copy functionality to code blocks:
import ReactMarkdown from 'react-markdown';
import { useState } from 'react';
function CodeBlock({ children, className }) {
const [copied, setCopied] = useState(false);
const code = String(children).replace(/\n$/, '');
const copyToClipboard = () => {
navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="code-block-wrapper">
<button onClick={copyToClipboard} className="copy-button">
{copied ? 'Copied!' : 'Copy'}
</button>
<pre className={className}>
<code>{code}</code>
</pre>
</div>
);
}
const components = {
code: ({node, inline, className, children, ...props}) => {
return inline ? (
<code className={className} {...props}>
{children}
</code>
) : (
<CodeBlock className={className} {...props}>
{children}
</CodeBlock>
);
},
};
<ReactMarkdown components={components}>
{content}
</ReactMarkdown>
Display line numbers in code blocks:
function CodeBlock({ children, className }) {
const code = String(children).replace(/\n$/, '');
const lines = code.split('\n');
return (
<pre className={className}>
<code>
{lines.map((line, i) => (
<span key={i} className="code-line">
<span className="line-number">{i + 1}</span>
<span className="line-content">{line}</span>
</span>
))}
</code>
</pre>
);
}
Implement lazy loading for images in markdown:
import { useState, useRef, useEffect } from 'react';
function LazyImage({ src, alt, ...props }) {
const [isLoaded, setIsLoaded] = useState(false);
const imgRef = useRef();
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsLoaded(true);
observer.disconnect();
}
},
{ threshold: 0.1 }
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => observer.disconnect();
}, []);
return (
<div ref={imgRef} className="lazy-image-container">
{isLoaded ? (
<img src={src} alt={alt} {...props} />
) : (
<div className="image-placeholder">Loading...</div>
)}
</div>
);
}
const components = {
img: ({node, src, alt, ...props}) => (
<LazyImage src={src} alt={alt} {...props} />
),
};
React-markdown v8+ fully supports React 18 with concurrent features:
// React 18 with Suspense
import { Suspense } from 'react';
import ReactMarkdown from 'react-markdown';
function App() {
return (
<Suspense fallback={<div>Loading markdown...</div>}>
<ReactMarkdown>{content}</ReactMarkdown>
</Suspense>
);
}
For React 17, use react-markdown v7 or earlier:
npm install react-markdown@7
Extract plain text from markdown for previews:
import { remark } from 'remark';
import stripMarkdown from 'strip-markdown';
async function extractText(markdown) {
const result = await remark()
.use(stripMarkdown)
.process(markdown);
return String(result);
}
Validate markdown before rendering:
import { remark } from 'remark';
async function validateMarkdown(markdown) {
try {
await remark().process(markdown);
return { valid: true };
} catch (error) {
return { valid: false, error: error.message };
}
}
Create custom markdown syntax with remark plugins:
import { visit } from 'unist-util-visit';
function remarkCustomAlert() {
return (tree) => {
visit(tree, 'text', (node) => {
// Custom processing logic
if (node.value.includes(':::alert')) {
// Transform custom syntax
}
});
};
}
<ReactMarkdown remarkPlugins={[remarkCustomAlert]}>
{content}
</ReactMarkdown>
React-markdown is an essential tool for any React developer working with markdown content. Its flexibility, security features, and extensive plugin ecosystem make it the go-to solution for rendering markdown in React applications.
Whether you're building a blog, documentation site, comment system, or content management platform, react-markdown provides the tools you need to render beautiful, formatted content from markdown text. With proper customization, security considerations, and performance optimization, you can create exceptional user experiences.
Start using react-markdown in your React applications today and unlock the power of markdown rendering in your projects!