
Description
Render Markdown source or a MarkdownDocument with @tanstack/markdown/react using Markdown, renderMarkdownReact, component replacements, and React static SSR. Load for React article components, emitted-tag mappings, pre-parsed documents, custom elements, or renderer parity.
SKILL.md
This skill builds on render-markdown. Read it first for the syntax profile, parser options, AST, and core trust boundaries.
React Rendering
Setup
Render source directly and keep the surrounding semantic element in the application:
import { Markdown } from '@tanstack/markdown/react'
export function Article({ source }: { source: string }) {
return (
<article>
<Markdown>{source}</Markdown>
</article>
)
}
Markdown returns a React fragment. The React adapter requires React 18 or newer.
Hooks and Components
Replace emitted links with an application component
import {
Markdown,
type MarkdownComponents,
} from '@tanstack/markdown/react'
const components = {
a(props) {
const external = props.href?.startsWith('http') ?? false
return (
<a
{...props}
rel={external ? 'nofollow noopener noreferrer' : props.rel}
target={external ? '_blank' : props.target}
/>
)
},
} satisfies MarkdownComponents
export function Article({ source }: { source: string }) {
return (
<article>
<Markdown components={components}>{source}</Markdown>
</article>
)
}
Known intrinsic keys infer their matching React props. Arbitrary extension component tag names remain supported.
Parse once before repeated React rendering
import { parseMarkdown } from '@tanstack/markdown/parser'
import { Markdown } from '@tanstack/markdown/react'
const document = parseMarkdown('# Cached article\n\nRendered from an AST.')
export function Article() {
return (
<article>
<Markdown>{document}</Markdown>
</article>
)
}
Apply parser options and document transforms when creating the document; renderer options cannot retroactively change its structure.
Render static React markup on the server
import { renderToStaticMarkup } from 'react-dom/server'
import { Markdown } from '@tanstack/markdown/react'
const source = '# Server-rendered article'
export const html = renderToStaticMarkup(
<article>
<Markdown>{source}</Markdown>
</article>,
)
Core static output is tested for structural equivalence with renderHtml().
Compose through the lower-level node API
import type { ReactNode } from 'react'
import { parseMarkdown } from '@tanstack/markdown/parser'
import { renderMarkdownReact } from '@tanstack/markdown/react'
const document = parseMarkdown('# Parsed once')
export function ArticleBody(): ReactNode {
return renderMarkdownReact(document)
}
Use renderBlockReact or renderInlineReact only when custom tree composition needs individual public AST nodes.
Common Mistakes
HIGH Injecting HTML instead of React nodes
Wrong:
import { renderHtml } from '@tanstack/markdown'
export function Article({ source }: { source: string }) {
const html = renderHtml(source)
return <article dangerouslySetInnerHTML={{ __html: html }} />
}
Correct:
import { Markdown } from '@tanstack/markdown/react'
export function Article({ source }: { source: string }) {
return (
<article>
<Markdown>{source}</Markdown>
</article>
)
}
The HTML-string path bypasses React component replacement and creates an unnecessary trusted-HTML boundary.
Source: docs/guides/react.md
HIGH Mapping AST node names instead of tags
Wrong:
import type { ComponentProps } from 'react'
import { Markdown } from '@tanstack/markdown/react'
function ArticleLink(props: ComponentProps<'a'>) {
return <a {...props} data-navigation="article" />
}
export function Article() {
return (
<Markdown components={{ link: ArticleLink }}>
{'Read the [guide](/guide).'}
</Markdown>
)
}
Correct:
import type { ComponentProps } from 'react'
import { Markdown } from '@tanstack/markdown/react'
function ArticleLink(props: ComponentProps<'a'>) {
return <a {...props} data-navigation="article" />
}
export function Article() {
return (
<Markdown components={{ a: ArticleLink }}>
{'Read the [guide](/guide).'}
</Markdown>
)
}
The map is keyed by emitted tag names such as a, not AST discriminants such as link.
Source: docs/guides/react.md
HIGH Expecting HTML extension hooks in React
Wrong:
import type { MarkdownExtension } from '@tanstack/markdown'
import { Markdown } from '@tanstack/markdown/react'
const paragraphHtml: MarkdownExtension = {
name: 'paragraph-html',
renderHtml(node) {
return node.type === 'paragraph'
? '<aside>Rendered only by the HTML renderer</aside>'
: undefined
},
}
export function Article() {
return <Markdown extensions={[paragraphHtml]}>Ordinary content</Markdown>
}
Correct:
import type { MarkdownExtension } from '@tanstack/markdown'
import type { PropsWithChildren } from 'react'
import { Markdown } from '@tanstack/markdown/react'
const panelExtension: MarkdownExtension = {
name: 'panel',
transformDocument(document) {
return {
...document,
children: [
{
type: 'component',
name: 'panel',
tagName: 'doc-panel',
attributes: {},
children: document.children,
},
],
}
},
}
function Panel({ children }: PropsWithChildren) {
return <aside className="documentation-panel">{children}</aside>
}
export function Article() {
return (
<Markdown
components={{ 'doc-panel': Panel }}
extensions={[panelExtension]}
>
Ordinary content
</Markdown>
)
}
MarkdownExtension.renderHtml is HTML-specific; portable custom output uses ComponentNode and an emitted-tag component mapping.
Source: docs/guides/extensions.md
CRITICAL Enabling raw HTML for untrusted input
Wrong:
import { Markdown } from '@tanstack/markdown/react'
export function UserPost({ source }: { source: string }) {
return <Markdown allowHtml>{source}</Markdown>
}
Correct:
import { Markdown } from '@tanstack/markdown/react'
export function UserPost({ source }: { source: string }) {
return <Markdown>{source}</Markdown>
}
With allowHtml, React uses dangerouslySetInnerHTML; TanStack Markdown does not sanitize that raw HTML.
Source: docs/core-concepts/security.md
HIGH Tension: renderer parity versus React customization
Core React SSR is tested against renderHtml(). React component replacements, raw HTML, highlighter output, and HTML-only extension hooks are application-controlled boundaries outside that parity guarantee.
See also: custom-extensions for portable ComponentNode output.
Cross-References
- render-markdown - shared
MarkdownInput, parser options, AST behavior, and syntax profile. - custom-extensions - emit custom tags that React can replace through
components. - production-pipelines - audit raw HTML, highlighter output, untrusted content, and SSR boundaries.
More skills from the markdown repository
View all 6 skillscustom-extensions
implement custom Markdown rendering extensions
Jul 26HTMLMarkdownPlugin Developmentdocs-features
build documentation features for Markdown
Jul 26DocumentationGitHubMarkdownTechnical Writingoctane-rendering
render Markdown with Octane
Jul 26FrontendMarkdownSSRTanStackproduction-pipelines
audit and ship production Markdown pipelines
Jul 26DeploymentMarkdownTanStackrender-markdown
parse and render Markdown documents
Jul 26DocumentationDocumentsHTMLMarkdown
More from TanStack
View publisheraggregation
perform data aggregation in TanStack Table
table
Jul 30Data AnalysisFrontendTanStackapi-not-found
diagnose TanStack Table API errors
table
Jul 30DebuggingFrontendTanStackcell-selection
select rectangular cell ranges in tables
table
Jul 30Data AnalysisTanStackUI Componentsclient-vs-server
manage TanStack Table data pipelines
table
Jul 30Data PipelineFrontendPerformanceTanStackcolumn-faceting
build faceted filter UIs
table
Jul 30Data VisualizationFrontendTanStackcolumn-filtering
implement column filtering in TanStack Table
table
Jul 30Data AnalysisFrontendTanStack