MDC Syntax Logo
Rendering

React

Learn how to use MDC Syntax in a React application.

Render MDC content in React applications with two components: MDC for simple use cases and MDCRenderer for advanced control.

Installation

npm install mdc-syntax
import { MDC, MDCRenderer } from 'mdc-syntax/react'

MDC Component

The MDC component is the simplest way to render markdown. Pass markdown content directly and it handles parsing and rendering automatically.

Basic Usage

import { MDC } from 'mdc-syntax/react'

const content = `# Hello World

This is **markdown** with MDC components.

::alert{type="info"}
This is an alert!
::
`

export default function App() {
  return <MDC markdown={content} />
}

Props

PropTypeDescription
markdownstringMarkdown content to parse and render
componentsRecord<string, ComponentType>Custom component mappings
excerptbooleanOnly render content before <!-- more -->
classNamestringCSS class for wrapper element

With Custom Components

import { MDC } from 'mdc-syntax/react'
import CustomAlert from './components/Alert'
import CustomCard from './components/Card'

const components = {
  alert: CustomAlert,
  card: CustomCard,
}

const content = `
::alert{type="warning"}
Important message here
::

::card{title="My Card"}
Card content
::
`

export default function App() {
  return <MDC markdown={content} components={components} />
}

Live Editor Example

import { useState } from 'react'
import { MDC } from 'mdc-syntax/react'

export default function MarkdownEditor() {
  const [content, setContent] = useState('# Edit me!\n\nType **markdown** here.')

  return (
    <div className="editor">
      <textarea
        value={content}
        onChange={(e) => setContent(e.target.value)}
      />
      <div className="preview">
        <MDC markdown={content} />
      </div>
    </div>
  )
}

Excerpt Mode

Render only content before <!-- more -->:

import { MDC } from 'mdc-syntax/react'

const content = `# Article Title

This is the excerpt shown in listings.

<!-- more -->

This is the full article content.
`

export default function ArticleCard() {
  // Only renders "This is the excerpt shown in listings."
  return <MDC markdown={content} excerpt />
}

With Tailwind CSS

<MDC
  value={content}
  className="prose prose-slate lg:prose-xl dark:prose-invert"
/>

MDCRenderer Component

The MDCRenderer component renders a pre-parsed Minimark AST. Use this when you need more control over parsing, want to cache parsed results, or are working with streams.

Basic Usage

import { parse } from 'mdc-syntax'
import { MDCRenderer } from 'mdc-syntax/react'

const content = `# Hello World

This is **markdown** with MDC components.
`

const result = parse(content)

export default function App() {
  return <MDCRenderer body={result.body} />
}

Props

PropTypeDescription
bodyMinimarkTreeRequired. The parsed AST to render
componentsRecord<string, ComponentType>Custom component mappings
componentsManifest`(name: string) => Promisenull`
streambooleanEnable streaming optimizations
classNamestringCSS class for wrapper element

With Custom Components

import { parse } from 'mdc-syntax'
import { MDCRenderer } from 'mdc-syntax/react'
import CustomAlert from './components/Alert'

const components = {
  alert: CustomAlert,
  h1: CustomHeading,
  h2: CustomHeading,
}

const result = parse(content)

export default function App() {
  return <MDCRenderer body={result.body} components={components} />
}

Streaming Support

MDCRenderer works with parseStreamIncremental for real-time content:

import { useState, useEffect } from 'react'
import { parseStreamIncremental } from 'mdc-syntax/stream'
import { MDCRenderer } from 'mdc-syntax/react'

export default function StreamingContent() {
  const [body, setBody] = useState({ type: 'minimark', value: [] })
  const [isComplete, setIsComplete] = useState(false)

  useEffect(() => {
    async function loadContent() {
      const response = await fetch('/api/content')

      for await (const result of parseStreamIncremental(response.body!)) {
        setBody(result.body)

        if (result.isComplete) {
          setIsComplete(true)
        }
      }
    }

    loadContent()
  }, [])

  return (
    <div>
      <MDCRenderer body={body} stream={!isComplete} />
      {!isComplete && <div className="loading">Streaming...</div>}
    </div>
  )
}

AI Chat Example

import { useState } from 'react'
import { parseStreamIncremental } from 'mdc-syntax/stream'
import { MDCRenderer } from 'mdc-syntax/react'

export default function Chat() {
  const [messages, setMessages] = useState([])
  const [currentBody, setCurrentBody] = useState({ type: 'minimark', value: [] })
  const [isStreaming, setIsStreaming] = useState(false)

  async function sendMessage(text: string) {
    setIsStreaming(true)

    const response = await fetch('/api/chat', {
      method: 'POST',
      body: JSON.stringify({ message: text }),
    })

    for await (const result of parseStreamIncremental(response.body!)) {
      setCurrentBody(result.body)

      if (result.isComplete) {
        setMessages(prev => [...prev, result.body])
        setCurrentBody({ type: 'minimark', value: [] })
        setIsStreaming(false)
      }
    }
  }

  return (
    <div>
      {messages.map((body, i) => (
        <MDCRenderer key={i} body={body} />
      ))}
      {isStreaming && <MDCRenderer body={currentBody} stream />}
    </div>
  )
}

Dynamic Component Resolution

For large applications, use componentsManifest for lazy loading:

import { Suspense } from 'react'
import { MDCRenderer } from 'mdc-syntax/react'

const componentMap: Record<string, () => Promise<any>> = {
  alert: () => import('./components/Alert'),
  card: () => import('./components/Card'),
  tabs: () => import('./components/Tabs'),
}

async function loadComponent(name: string) {
  const loader = componentMap[name]
  return loader ? loader() : { default: ({ children }) => <div>{children}</div> }
}

export default function App({ mdcAst }) {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <MDCRenderer body={mdcAst} componentsManifest={loadComponent} />
    </Suspense>
  )
}

Custom Components

Both MDC and MDCRenderer support custom components. Components receive props from the markdown and children as React children.

Creating a Custom Alert

// components/Alert.tsx
interface AlertProps {
  type?: 'info' | 'warning' | 'error' | 'success'
  children: React.ReactNode
}

export default function Alert({ type = 'info', children }: AlertProps) {
  return (
    <div className={`alert alert-${type}`} role="alert">
      {children}
    </div>
  )
}

Usage in markdown:

::alert{type="warning"}
This is a warning message!
::

Component with Complex Props

// components/DataTable.tsx
interface DataTableProps {
  columns?: string[]    // From {:columns='["Name","Age"]'}
  sortable?: boolean    // From {sortable}
  children: React.ReactNode
}

export default function DataTable({ columns = [], sortable, children }: DataTableProps) {
  return (
    <table>
      {columns.length > 0 && (
        <thead>
          <tr>
            {columns.map((col, i) => (
              <th key={i}>{col}</th>
            ))}
          </tr>
        </thead>
      )}
      <tbody>{children}</tbody>
    </table>
  )
}

Usage in markdown:

::data-table{:columns='["Name", "Age"]' sortable}
Table content here
::

Overriding HTML Elements

Override default element rendering:

// components/Heading.tsx
interface HeadingProps {
  __node?: any
  id?: string
  children: React.ReactNode
}

export default function Heading({ __node, id, children }: HeadingProps) {
  const Tag = __node?.[0] || 'h2'

  return (
    <Tag id={id} className="heading">
      {id && <a href={`#${id}`} className="anchor">#</a>}
      {children}
    </Tag>
  )
}
const components = {
  h1: Heading,
  h2: Heading,
  h3: Heading,
}

<MDC markdown={content} components={components} />

Props Conversion

React renderer automatically converts attributes:

MarkdownReact
{class="foo"}className="foo"
{tabindex="0"}tabIndex={0}
{style="color: red"}style={{ color: 'red' }}
{:count="5"}count={5} (number)
{:data='{"key":"val"}'}data={{ key: "val" }} (object)

ShikiCodeBlock

Syntax-highlighted code block component using Shiki:

import { ShikiCodeBlock } from 'mdc-syntax/react'

export default function App() {
  return (
    <ShikiCodeBlock
      language="typescript"
      theme={{ light: 'github-light', dark: 'github-dark' }}
    >
      {`const greeting = "Hello, World!"`}
    </ShikiCodeBlock>
  )
}

ProsePre & ProsePreShiki

Enhanced code block components with copy functionality and styled headers. These components are part of the internal prose components system (src/react/components/prose/) and are used automatically when rendering code blocks.

Component Variants

ProsePre: Code block without syntax highlighting but with copy functionality ProsePreShiki: Code block with Shiki syntax highlighting, copy functionality, and styled header

Features

  • Copy to Clipboard: Click the copy button to copy code content
  • Language Label: Displays language or filename in the header
  • Hover Effects: Copy button appears on hover with smooth transitions
  • Loading States: ProsePreShiki shows loading state while highlighter initializes
  • Fallback: Graceful fallback to plain code if highlighting fails
  • Singleton Highlighter: Shared highlighter instance for performance

These components are available in the source code at packages/mdc-syntax/src/react/components/prose/ and are used internally by the prose rendering system.


TypeScript Support

import type { MinimarkTree } from 'mdc-syntax'
import { MDC, MDCRenderer } from 'mdc-syntax/react'

interface Props {
  content: string
}

export default function Content({ content }: Props) {
  return <MDC markdown={content} className="prose" />
}

// Custom component types
interface MDCComponentProps {
  __node?: any
  children?: React.ReactNode
}

interface AlertProps extends MDCComponentProps {
  type?: 'info' | 'warning' | 'error' | 'success'
}

Performance Tips

  1. Use MDC for simple cases - It handles parsing internally and re-parses efficiently on content changes.
  2. Use MDCRenderer with caching - Pre-parse content and memoize the AST:
const mdcAst = useMemo(() => parse(content).body, [content])
  1. Memoize component mappings:
// Good - defined outside component
const components = { alert: Alert, card: Card }

// Or use useMemo
const components = useMemo(() => ({ alert: Alert }), [])
  1. Use React.memo for custom components:
export default React.memo(function Alert({ type, children }) {
  return <div className={`alert-${type}`}>{children}</div>
})

See Also