e0a5f57793
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as a monorepo subdirectory under desktop/. Source only — node_modules, dist, Cargo target and the upstream .git history are excluded. desktop/ keeps its own independent Cargo workspace (desktop/src-tauri with crates/*); it is not part of the backend boosapi crate, so a `cargo check` at the metapi root still compiles only the backend. Build the desktop app from desktop/src-tauri per desktop/README.md. Note: app icons are still placeholder assets pending the final BoosAPI logo. The updater signing keys live outside the repo and are not included.
750 lines
20 KiB
Markdown
750 lines
20 KiB
Markdown
# React Components & API
|
||
|
||
markstream-react provides the same powerful components as markstream-vue, but built for React. All components support React 18+ with full TypeScript support.
|
||
|
||
The root `markstream-react`, `markstream-react/next`, and `markstream-react/server` entrypoints all ship declaration files. Shared renderer and component types such as `NodeRendererProps`, `NodeRendererCodeBlockProps`, `NodeComponentProps`, `RenderContext`, `RenderNodeFn`, `CustomComponentMap`, `CodeBlockMonacoOptions`, `MarkdownCodeBlockNodeProps`, `ListItemNodeProps`, `HtmlPreviewFrameProps`, `TooltipProps`, `TooltipPlacement`, and `LinkNodeStyleProps` can be imported directly from the entrypoint you use.
|
||
## Main Component: MarkdownRender
|
||
|
||
The primary component for rendering markdown content in React.
|
||
|
||
### Props
|
||
|
||
`MarkdownRender` uses the `NodeRendererProps` interface from `markstream-react`.
|
||
|
||
#### Core props
|
||
|
||
| Prop | Type | Default | Description |
|
||
|------|------|---------|-------------|
|
||
| `content` | `string` | - | Markdown content to render |
|
||
| `nodes` | `BaseNode[]` | - | Pre-parsed AST nodes (typically `ParsedNode[]` from the parser) |
|
||
| `customId` | `string` | - | Identifier for scoping custom components and CSS |
|
||
| `final` | `boolean` | `false` | Marks end-of-stream; stops emitting streaming `loading` nodes |
|
||
| `parseOptions` | `ParseOptions` | - | Parser options and token hooks (only when `content` is provided) |
|
||
| `customHtmlTags` | `readonly string[]` | - | HTML-like tags emitted as custom nodes (e.g. `thinking`) |
|
||
| `customMarkdownIt` | `(md: MarkdownIt) => MarkdownIt` | - | Customize the internal MarkdownIt instance |
|
||
| `debugPerformance` | `boolean` | `false` | Log parse/render timing and virtualization stats (dev only) |
|
||
| `isDark` | `boolean` | `false` | Theme flag forwarded to heavy nodes; adds `.dark` to the root container |
|
||
| `indexKey` | `number \| string` | - | Key prefix when rendering multiple instances in lists |
|
||
| `typewriter` | `boolean` | `true` | Enable the non-code-node enter transition |
|
||
| `showTooltips` | `boolean` | `true` | Global tooltip switch for `LinkNode` and code block nodes |
|
||
|
||
#### Streaming & heavy-node toggles
|
||
|
||
| Prop | Default | Description |
|
||
|------|---------|-------------|
|
||
| `renderCodeBlocksAsPre` | `false` | Render code blocks as `<pre><code>` (Mermaid/D2/Infographic blocks will also fall back) |
|
||
| `codeBlockStream` | `true` | Stream code block updates as content arrives |
|
||
| `viewportPriority` | `true` | Defer heavy work (Monaco/Mermaid/D2/KaTeX) until near viewport |
|
||
| `deferNodesUntilVisible` | `true` | Render heavy nodes as placeholders until visible (non-virtualized mode only) |
|
||
|
||
#### Performance (virtualization & batching)
|
||
|
||
| Prop | Default | Description |
|
||
|------|---------|-------------|
|
||
| `maxLiveNodes` | `320` | Max fully rendered nodes kept in DOM (set `0` to disable virtualization) |
|
||
| `liveNodeBuffer` | `60` | Overscan buffer around the focus range |
|
||
| `batchRendering` | `true` | Incremental batch rendering when virtualization is disabled |
|
||
| `initialRenderBatchSize` | `40` | Nodes rendered immediately before batching starts |
|
||
| `renderBatchSize` | `80` | Nodes rendered per batch tick |
|
||
| `renderBatchDelay` | `16` | Extra delay (ms) before each batch after rAF |
|
||
| `renderBatchBudgetMs` | `6` | Time budget (ms) before adaptive batch sizes shrink |
|
||
| `renderBatchIdleTimeoutMs` | `120` | Timeout (ms) for `requestIdleCallback` slices |
|
||
|
||
#### Global code block options
|
||
|
||
| Prop | Type | Description |
|
||
|------|------|-------------|
|
||
| `codeBlockDarkTheme` | `CodeBlockMonacoTheme` | Monaco dark theme object forwarded to every `CodeBlockNode` |
|
||
| `codeBlockLightTheme` | `CodeBlockMonacoTheme` | Monaco light theme object forwarded to every `CodeBlockNode` |
|
||
| `codeBlockMonacoOptions` | `CodeBlockMonacoOptions` | Options forwarded to `stream-monaco`, including diff hover-action settings like `diffHunkActionsOnHover`, `diffHunkHoverHideDelayMs`, and `onDiffHunkAction` |
|
||
| `codeBlockMinWidth` | `string \| number` | Min width forwarded to `CodeBlockNode` |
|
||
| `codeBlockMaxWidth` | `string \| number` | Max width forwarded to `CodeBlockNode` |
|
||
| `codeBlockProps` | `NodeRendererCodeBlockProps` | Extra props forwarded to every code-block renderer (`CodeBlockNode` / `MarkdownCodeBlockNode`) |
|
||
| `mermaidProps` | `Partial<Omit<MermaidBlockNodeProps, 'node' \| 'loading' \| 'isDark'>>` | Extra props forwarded to Mermaid fences and custom `mermaid` renderers |
|
||
| `d2Props` | `Partial<Omit<D2BlockNodeProps, 'node' \| 'loading' \| 'isDark'>>` | Extra props forwarded to D2 fences and custom `d2` renderers |
|
||
| `infographicProps` | `Partial<Omit<InfographicBlockNodeProps, 'node' \| 'loading' \| 'isDark'>>` | Extra props forwarded to infographic fences and custom `infographic` renderers |
|
||
| `themes` | `CodeBlockMonacoTheme[]` | Theme list forwarded to `stream-monaco` |
|
||
|
||
#### Heavy renderer prop forwarding
|
||
|
||
`NodeRenderer` can tune heavy blocks directly:
|
||
|
||
```tsx
|
||
<MarkdownRender
|
||
content={markdown}
|
||
viewportPriority
|
||
mermaidProps={{
|
||
showHeader: false,
|
||
renderDebounceMs: 180,
|
||
previewPollDelayMs: 500,
|
||
}}
|
||
d2Props={{ progressiveIntervalMs: 500 }}
|
||
infographicProps={{ showHeader: false }}
|
||
/>
|
||
```
|
||
|
||
Streaming notes:
|
||
- Keep `viewportPriority` enabled to prevent offscreen Mermaid / Monaco / D2 work from running while text is still streaming.
|
||
- For high-frequency SSE, prefer passing `nodes` instead of reparsing the full `content` string every chunk.
|
||
- Common Mermaid tuning keys: `renderDebounceMs`, `contentStableDelayMs`, `previewPollDelayMs`, `previewPollMaxDelayMs`, `previewPollMaxAttempts`.
|
||
|
||
`NodeRendererCodeBlockProps` follows the public `CodeBlockNode` prop surface except for `node`, so you get completion for flags like `showHeader`, `showFontSizeButtons`, and `showTooltips` without dropping to `any`.
|
||
|
||
```tsx
|
||
import type { NodeRendererCodeBlockProps } from 'markstream-react'
|
||
|
||
const codeBlockProps: NodeRendererCodeBlockProps = {
|
||
showHeader: false,
|
||
showFontSizeButtons: false,
|
||
showTooltips: false,
|
||
}
|
||
```
|
||
|
||
#### Events
|
||
|
||
| Prop | Type | Description |
|
||
|------|------|-------------|
|
||
| `onCopy` | `(code: string) => void` | Fired when code blocks emit copy events |
|
||
| `onHandleArtifactClick` | `(payload: any) => void` | Fired on artifact/preview clicks |
|
||
| `onClick` | `(event: React.MouseEvent<HTMLDivElement>) => void` | Click handler for the renderer root |
|
||
| `onMouseOver` | `(event: React.MouseEvent<HTMLElement>) => void` | Mouseover handler for the renderer root |
|
||
| `onMouseOut` | `(event: React.MouseEvent<HTMLElement>) => void` | Mouseout handler for the renderer root |
|
||
|
||
### Usage
|
||
|
||
```tsx
|
||
import MarkdownRender from 'markstream-react'
|
||
|
||
function App() {
|
||
const markdown = `# Hello React!
|
||
|
||
This is markstream-react.`
|
||
|
||
return (
|
||
<MarkdownRender
|
||
customId="docs"
|
||
content={markdown}
|
||
maxLiveNodes={150}
|
||
/>
|
||
)
|
||
}
|
||
```
|
||
|
||
## Code Block Components
|
||
|
||
### MarkdownCodeBlockNode
|
||
|
||
Lightweight code highlighting using Shiki.
|
||
|
||
```tsx
|
||
import { MarkdownCodeBlockNode } from 'markstream-react'
|
||
|
||
function CodeBlock() {
|
||
const codeNode = {
|
||
type: 'code_block',
|
||
language: 'javascript',
|
||
code: 'const hello = "world"',
|
||
raw: 'const hello = "world"'
|
||
}
|
||
|
||
const handleCopy = () => {
|
||
alert('Code copied!')
|
||
}
|
||
|
||
return (
|
||
<div className="markstream-react">
|
||
<MarkdownCodeBlockNode
|
||
node={codeNode}
|
||
showCopyButton={true}
|
||
showCollapseButton={false}
|
||
onCopy={handleCopy}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
### CodeBlockNode
|
||
|
||
Feature-rich Monaco-powered code blocks.
|
||
|
||
```tsx
|
||
import { CodeBlockNode } from 'markstream-react'
|
||
|
||
function MonacoCodeBlock() {
|
||
const codeNode = {
|
||
type: 'code_block',
|
||
language: 'typescript',
|
||
code: 'const greeting: string = "Hello"',
|
||
raw: 'const greeting: string = "Hello"'
|
||
}
|
||
|
||
const handleCopy = (code: string) => {
|
||
console.log('Code copied:', code)
|
||
}
|
||
|
||
const handlePreviewCode = (artifact: any) => {
|
||
console.log('Preview code:', artifact)
|
||
}
|
||
|
||
return (
|
||
<div className="markstream-react">
|
||
<CodeBlockNode
|
||
node={codeNode}
|
||
monacoOptions={{ fontSize: 14, theme: 'vs-dark' }}
|
||
stream={true}
|
||
showCollapseButton={false}
|
||
onCopy={handleCopy}
|
||
onPreviewCode={handlePreviewCode}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
## Math Components
|
||
|
||
### MathBlockNode
|
||
|
||
Renders block-level math formulas with KaTeX.
|
||
|
||
```tsx
|
||
import { MathBlockNode } from 'markstream-react'
|
||
|
||
function MathBlock() {
|
||
const mathNode = {
|
||
type: 'math_block',
|
||
content: '\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}',
|
||
raw: '\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}'
|
||
}
|
||
|
||
return (
|
||
<div className="markstream-react">
|
||
<MathBlockNode node={mathNode} />
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
### MathInlineNode
|
||
|
||
Renders inline math formulas.
|
||
|
||
```tsx
|
||
import { MathInlineNode } from 'markstream-react'
|
||
|
||
function MathInline() {
|
||
const inlineMathNode = {
|
||
type: 'math_inline',
|
||
content: 'E = mc^2',
|
||
raw: 'E = mc^2'
|
||
}
|
||
|
||
return (
|
||
<div className="markstream-react">
|
||
<p>
|
||
The formula is:
|
||
{' '}
|
||
<MathInlineNode node={inlineMathNode} />
|
||
</p>
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
## Mermaid Diagrams
|
||
|
||
### MermaidBlockNode
|
||
|
||
Progressive Mermaid diagram rendering.
|
||
|
||
#### Quick reference
|
||
- **Tooltip control**: `showTooltips` defaults to `true`; set `false` to disable header action tooltips.
|
||
|
||
```tsx
|
||
import { MermaidBlockNode } from 'markstream-react'
|
||
|
||
function MermaidDiagram() {
|
||
const mermaidNode = {
|
||
type: 'code_block',
|
||
language: 'mermaid',
|
||
code: `graph TD
|
||
A[Start] --> B{Is it working?}
|
||
B -->|Yes| C[Great!]`,
|
||
raw: ''
|
||
}
|
||
|
||
const handleExport = (ev: any) => {
|
||
console.log('Mermaid SVG:', ev.svgString)
|
||
}
|
||
|
||
return (
|
||
<div className="markstream-react">
|
||
<MermaidBlockNode
|
||
node={mermaidNode}
|
||
isStrict={true}
|
||
onExport={handleExport}
|
||
showTooltips={true}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
Event notes:
|
||
- `onCopy(code: string)` receives the source text directly (no `MermaidBlockEvent` wrapper in React).
|
||
- `onExport`, `onOpenModal`, `onToggleMode` receive `MermaidBlockEvent` and support `ev.preventDefault()` to stop the default behavior.
|
||
- `onToggleMode` signature: `(target: 'source' | 'preview', ev)`.
|
||
- For renderer-wide usage, prefer passing these knobs through `mermaidProps` on `MarkdownRender` / `NodeRenderer`.
|
||
|
||
## D2 Diagrams
|
||
|
||
### D2BlockNode
|
||
|
||
Progressive D2 diagram rendering with source fallback.
|
||
|
||
```tsx
|
||
import { D2BlockNode } from 'markstream-react'
|
||
|
||
function D2Diagram() {
|
||
const d2Node = {
|
||
type: 'code_block',
|
||
language: 'd2',
|
||
code: `direction: right
|
||
Client -> API: request
|
||
API -> DB: query`,
|
||
raw: ''
|
||
}
|
||
|
||
return (
|
||
<div className="markstream-react">
|
||
<D2BlockNode
|
||
node={d2Node}
|
||
progressiveIntervalMs={600}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
## Other Node Components
|
||
|
||
### HeadingNode
|
||
|
||
```tsx
|
||
import { HeadingNode } from 'markstream-react'
|
||
|
||
function CustomHeading() {
|
||
const headingNode = {
|
||
type: 'heading',
|
||
level: 1
|
||
}
|
||
|
||
return <HeadingNode node={headingNode}>Hello World</HeadingNode>
|
||
}
|
||
```
|
||
|
||
### ParagraphNode
|
||
|
||
```tsx
|
||
import { ParagraphNode } from 'markstream-react'
|
||
|
||
function CustomParagraph() {
|
||
const paragraphNode = {
|
||
type: 'paragraph'
|
||
}
|
||
|
||
return (
|
||
<ParagraphNode node={paragraphNode}>
|
||
This is a
|
||
{' '}
|
||
<strong>bold</strong>
|
||
{' '}
|
||
word.
|
||
</ParagraphNode>
|
||
)
|
||
}
|
||
```
|
||
|
||
### ListNode
|
||
|
||
```tsx
|
||
import { ListNode, renderNode } from 'markstream-react'
|
||
|
||
function CustomList() {
|
||
const listNode = {
|
||
type: 'list',
|
||
ordered: false,
|
||
items: [
|
||
{
|
||
type: 'list_item',
|
||
children: [
|
||
{ type: 'paragraph', children: [{ type: 'text', content: 'Item 1' }] }
|
||
]
|
||
},
|
||
{
|
||
type: 'list_item',
|
||
children: [
|
||
{ type: 'paragraph', children: [{ type: 'text', content: 'Item 2' }] }
|
||
]
|
||
}
|
||
]
|
||
}
|
||
|
||
const ctx = { events: {} }
|
||
|
||
return <ListNode node={listNode} ctx={ctx} renderNode={renderNode} />
|
||
}
|
||
```
|
||
|
||
### LinkNode
|
||
|
||
```tsx
|
||
import { LinkNode } from 'markstream-react'
|
||
|
||
function CustomLink() {
|
||
const linkNode = {
|
||
type: 'link',
|
||
href: 'https://example.com',
|
||
title: 'Example',
|
||
text: 'Click me'
|
||
}
|
||
|
||
return (
|
||
<LinkNode
|
||
node={linkNode}
|
||
color="#e11d48"
|
||
underlineHeight={3}
|
||
showTooltip={true}
|
||
/>
|
||
)
|
||
}
|
||
```
|
||
|
||
### ImageNode
|
||
|
||
```tsx
|
||
import { ImageNode } from 'markstream-react'
|
||
|
||
function CustomImage() {
|
||
const imageNode = {
|
||
type: 'image',
|
||
src: 'https://example.com/image.jpg',
|
||
alt: 'Example image',
|
||
title: 'Example',
|
||
raw: ''
|
||
}
|
||
|
||
const handleClick = () => {
|
||
console.log('Image clicked!')
|
||
}
|
||
|
||
const handleLoad = () => {
|
||
console.log('Image loaded!')
|
||
}
|
||
|
||
return (
|
||
<ImageNode
|
||
node={imageNode}
|
||
onClick={handleClick}
|
||
onLoad={handleLoad}
|
||
/>
|
||
)
|
||
}
|
||
```
|
||
|
||
## Utility Functions
|
||
|
||
### getMarkdown
|
||
|
||
Get a configured markdown-it instance.
|
||
|
||
```tsx
|
||
import { getMarkdown } from 'stream-markdown-parser'
|
||
|
||
const md = getMarkdown('my-msg-id', {
|
||
html: true,
|
||
linkify: true,
|
||
typographer: true
|
||
})
|
||
|
||
const tokens = md.parse('# Hello World')
|
||
```
|
||
|
||
### parseMarkdownToStructure
|
||
|
||
Parse markdown string to AST structure.
|
||
|
||
```tsx
|
||
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
|
||
|
||
const md = getMarkdown()
|
||
const nodes = parseMarkdownToStructure('# Title\n\nContent here...', md)
|
||
|
||
// Use with MarkdownRender
|
||
// <MarkdownRender nodes={nodes} />
|
||
```
|
||
|
||
### Optional workers (Mermaid / KaTeX)
|
||
|
||
Mermaid/KaTeX auto-load when installed. If you want off‑thread parsing/rendering, inject workers:
|
||
|
||
```tsx
|
||
import { setKaTeXWorker, setMermaidWorker } from 'markstream-react'
|
||
import KatexWorker from 'markstream-react/workers/katexRenderer.worker?worker'
|
||
import MermaidWorker from 'markstream-react/workers/mermaidParser.worker?worker'
|
||
|
||
setMermaidWorker(new MermaidWorker())
|
||
setKaTeXWorker(new KatexWorker())
|
||
```
|
||
|
||
## Custom Component API
|
||
|
||
### Props Interface
|
||
|
||
All custom node components receive these props:
|
||
|
||
```tsx
|
||
import type { CustomComponentMap, NodeComponentProps, RenderContext, RenderNodeFn } from 'markstream-react'
|
||
|
||
interface NodeComponentProps<TNode = unknown> {
|
||
node: TNode // The parsed node data
|
||
ctx?: RenderContext // Renderer context (themes, events, flags)
|
||
renderNode?: RenderNodeFn // Helper to render child nodes
|
||
indexKey?: React.Key // Unique key for the node
|
||
customId?: string // Custom ID for scoping
|
||
isDark?: boolean
|
||
typewriter?: boolean
|
||
children?: React.ReactNode
|
||
}
|
||
```
|
||
|
||
`CustomComponentMap` is the typed mapping shape accepted by `setCustomComponents(...)`.
|
||
|
||
### Example Custom Component
|
||
|
||
```tsx
|
||
import React from 'react'
|
||
|
||
interface CustomParagraphProps {
|
||
node: {
|
||
type: string
|
||
children: Array<{
|
||
type: string
|
||
content?: string
|
||
children?: any[]
|
||
}>
|
||
}
|
||
indexKey?: number | string
|
||
customId?: string
|
||
}
|
||
|
||
function CustomParagraph({ node, indexKey, customId }: CustomParagraphProps) {
|
||
return (
|
||
<p
|
||
className={`custom-paragraph custom-paragraph-${indexKey}`}
|
||
data-custom-id={customId}
|
||
data-node-type={node.type}
|
||
>
|
||
{node.children.map((child, i) => (
|
||
<span key={i}>{child.content || ''}</span>
|
||
))}
|
||
</p>
|
||
)
|
||
}
|
||
|
||
// Usage
|
||
function App() {
|
||
const paragraphNode = {
|
||
type: 'paragraph',
|
||
children: [
|
||
{ type: 'text', content: 'Custom paragraph content' }
|
||
]
|
||
}
|
||
|
||
return <CustomParagraph node={paragraphNode} indexKey={0} customId="docs" />
|
||
}
|
||
```
|
||
|
||
## Context + Custom Components
|
||
|
||
You can use React Context inside custom node components, while still registering them via `setCustomComponents`:
|
||
|
||
```tsx
|
||
import MarkdownRender, { setCustomComponents } from 'markstream-react'
|
||
import React, { createContext, useContext } from 'react'
|
||
|
||
const ThemeContext = createContext<'light' | 'dark'>('light')
|
||
|
||
function CustomHeading({ node, customId }: any) {
|
||
const theme = useContext(ThemeContext)
|
||
const level = node.level || 1
|
||
const Tag = `h${level}` as keyof JSX.IntrinsicElements
|
||
|
||
return (
|
||
<Tag className={`custom-heading ${theme}`} data-custom-id={customId}>
|
||
{node.children?.map((child: any, i: number) => (
|
||
<span key={i}>{child.content || ''}</span>
|
||
))}
|
||
</Tag>
|
||
)
|
||
}
|
||
|
||
setCustomComponents('docs', { heading: CustomHeading })
|
||
|
||
function App() {
|
||
const markdown = `# Custom Heading
|
||
|
||
This uses a custom heading component.
|
||
`
|
||
|
||
return (
|
||
<ThemeContext.Provider value="dark">
|
||
<MarkdownRender customId="docs" content={markdown} />
|
||
</ThemeContext.Provider>
|
||
)
|
||
}
|
||
```
|
||
|
||
## Streaming Support
|
||
|
||
markstream-react supports streaming markdown content:
|
||
|
||
```tsx
|
||
import MarkdownRender from 'markstream-react'
|
||
import { useEffect, useState } from 'react'
|
||
|
||
function StreamingDemo() {
|
||
const [content, setContent] = useState('')
|
||
const fullContent = `# Streaming Demo
|
||
|
||
This content streams in **small chunks**.
|
||
`
|
||
|
||
useEffect(() => {
|
||
let i = 0
|
||
const interval = setInterval(() => {
|
||
if (i < fullContent.length) {
|
||
setContent(prev => prev + fullContent[i])
|
||
i++
|
||
}
|
||
else {
|
||
clearInterval(interval)
|
||
}
|
||
}, 30)
|
||
|
||
return () => clearInterval(interval)
|
||
}, [])
|
||
|
||
return <MarkdownRender content={content} />
|
||
}
|
||
```
|
||
|
||
## TypeScript Support
|
||
|
||
markstream-react includes full TypeScript definitions:
|
||
|
||
```tsx
|
||
import type { NodeComponentProps, NodeRendererProps } from 'markstream-react'
|
||
import type { ParsedNode } from 'stream-markdown-parser'
|
||
import MarkdownRender from 'markstream-react'
|
||
|
||
function App() {
|
||
const markdown = '# Hello TypeScript!'
|
||
const nodes: ParsedNode[] = []
|
||
|
||
return <MarkdownRender content={markdown} nodes={nodes} />
|
||
}
|
||
```
|
||
|
||
Code block prop interfaces (`CodeBlockNodeProps`, `MermaidBlockNodeProps`, `D2BlockNodeProps`, `InfographicBlockNodeProps`, `PreCodeNodeProps`) all use `node: CodeBlockNode` from `stream-markdown-parser` (use `language: 'mermaid'` / `language: 'd2'` / `language: 'd2lang'` / `language: 'infographic'` when targeting specialized renderers).
|
||
|
||
## Next.js Best Practices
|
||
|
||
Use the dedicated Next SSR entrypoints instead of `mounted` guards or `ssr: false`.
|
||
|
||
```tsx
|
||
import MarkdownRender from 'markstream-react/next'
|
||
|
||
export default function MarkdownPage() {
|
||
return <MarkdownRender content="# Hello Next.js!" final />
|
||
}
|
||
```
|
||
|
||
For a pure server render path:
|
||
|
||
```tsx
|
||
import MarkdownRender from 'markstream-react/server'
|
||
|
||
export default function MarkdownPage() {
|
||
return <MarkdownRender content="# Hello!" final />
|
||
}
|
||
```
|
||
|
||
See [React Next SSR](/guide/react-next-ssr) for the full entrypoint model.
|
||
|
||
## Hooks Integration
|
||
|
||
You can easily integrate with React hooks:
|
||
|
||
```tsx
|
||
import type { ChangeEvent } from 'react'
|
||
import MarkdownRender from 'markstream-react'
|
||
import { useCallback, useMemo, useState } from 'react'
|
||
|
||
function MarkdownEditor() {
|
||
const [content, setContent] = useState('# Edit me!')
|
||
const [theme, setTheme] = useState('light')
|
||
|
||
const memoizedContent = useMemo(() => content, [content])
|
||
|
||
const handleChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
||
setContent(e.target.value)
|
||
}, [])
|
||
|
||
return (
|
||
<div>
|
||
<textarea value={content} onChange={handleChange} />
|
||
<MarkdownRender
|
||
content={memoizedContent}
|
||
customId={`editor-${theme}`}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
## Error Handling
|
||
|
||
```tsx
|
||
import MarkdownRender from 'markstream-react'
|
||
import { useState } from 'react'
|
||
|
||
function SafeMarkdown({ content }: { content: string }) {
|
||
const [error, setError] = useState<Error | null>(null)
|
||
|
||
if (error) {
|
||
return (
|
||
<div>
|
||
Error rendering markdown:
|
||
{error.message}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
try {
|
||
return <MarkdownRender content={content} />
|
||
}
|
||
catch (err) {
|
||
setError(err as Error)
|
||
return null
|
||
}
|
||
}
|
||
```
|
||
|
||
## Next Steps
|
||
|
||
- See [React Quick Start](/guide/react-quick-start) for setup examples
|
||
- Explore [Vue 3 Components](/guide/components) for more component examples (API is similar)
|
||
- Check [Usage & API](/guide/usage) for advanced patterns
|