) {
return (
{node.children?.map((child: any, i: number) =>
renderNode && ctx
? renderNode(child, `${String(indexKey)}-link-${i}`, ctx)
: null,
)}
)
}
setCustomComponents('docs', {
heading: CustomHeading,
link: CustomLink,
})
export function Article({ markdown }: { markdown: string }) {
return
}
```
Useful node-type translations:
- `h1` ... `h6` -> `heading` + `node.level`
- `a` -> `link`
- `p` -> `paragraph`
- `img` -> `image`
- `code` / `pre` -> `code_block` or `inline_code`
- Custom HTML-like tags -> `customHtmlTags` + `setCustomComponents`
## Migrating code highlighting
Many `react-markdown` apps override `components.code` and wire in a third-party highlighter.
With `markstream-react`, you have three common choices:
- Keep the default `CodeBlockNode` for Monaco-powered code blocks.
- Swap `code_block` to `MarkdownCodeBlockNode` for a lighter Shiki-based renderer.
- Set `renderCodeBlocksAsPre` when you want plain ``.
Example: swap code blocks to Shiki:
```tsx
import MarkdownRender, { MarkdownCodeBlockNode, setCustomComponents } from 'markstream-react'
setCustomComponents('docs', {
code_block: ({ node, isDark, ctx }: any) => (
),
})
export function Article({ markdown }: { markdown: string }) {
return
}
```
## Migrating plugin logic
### `remarkPlugins`
If you used `remarkPlugins` for syntax extensions, the nearest equivalent is `customMarkdownIt`.
```tsx
import type { MarkdownIt } from 'markdown-it-ts'
import { full as markdownItEmoji } from 'markdown-it-emoji'
import MarkdownRender from 'markstream-react'
function withEmoji(md: MarkdownIt) {
md.use(markdownItEmoji)
return md
}
export function Article({ markdown }: { markdown: string }) {
return
}
```
### `rehypePlugins`
There is no direct `rehype` step in `markstream-react`.
If your old `rehype` logic was used for:
- HTML tree rewrites: move that logic into custom node renderers or node post-processing.
- Custom HTML-like blocks: prefer `customHtmlTags`.
- Security allow/deny logic: use `parseOptions.validateLink`, custom renderers, or node filtering.
## HTML, custom tags, and security
`react-markdown` usually escapes HTML unless you add something like `rehypeRaw`.
`markstream-react` already parses HTML-like nodes, and its built-in HTML renderers strip blocked tags, dangerous event-handler attributes, and unsafe URLs.
If your old `rehypeRaw` usage was mainly there to support custom tags such as ``, this is the recommended migration path:
```tsx
import type { NodeComponentProps } from 'markstream-react'
import MarkdownRender, { setCustomComponents } from 'markstream-react'
function ThinkingNode({ node }: NodeComponentProps) {
return
}
setCustomComponents('chat', { thinking: ThinkingNode })
export function Message({ markdown }: { markdown: string }) {
return (
)
}
```
## Streaming upgrade path
You do not need to adopt streaming on day one. A practical migration path is:
1. Replace `react-markdown` with `MarkdownRender` and keep using `content`.
2. Migrate custom renderers and plugin logic.
3. If you later render SSE/chat output, parse outside the component and pass `nodes`.
```tsx
import MarkdownRender from 'markstream-react'
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
const md = getMarkdown('chat')
const nodes = parseMarkdownToStructure(buffer, md, { final: done })
export function Message() {
return (
)
}
```
For high-frequency streaming, this `nodes` flow is usually the biggest reason to migrate.
## Migration checklist
- Replace `{markdown}` with ``.
- Import `markstream-react/index.css`.
- Move `components` overrides into `setCustomComponents(customId, mapping)`.
- Remove plugins that are now redundant before re-adding custom ones.
- Re-check any `rehype`-based HTML filtering or transformation logic.
- If your app renders incremental output, move from `content` to parsed `nodes`.
## Related docs
- [React Installation](/guide/react-installation)
- [React Components & API](/guide/react-components)
- [Parser API](/guide/parser-api)