# React integration

React components, streaming, SSR and custom nodes sharing the Vue parser core.

`@quirekit/incremark-react` provides React 19 `QuireMarkdown` directly over
`@quirekit/incremark-core`. Parsing, late references, footnotes, HTML sanitization and
cache rules are shared with Vue. The React package renders its own semantic DOM and
does not require Vue, QuireKit UI or UnoCSS.

## Workspace integration

The packages are not on npm yet. In a React application inside the QuireKit workspace:

```bash
pnpm add '@quirekit/incremark-react@workspace:*' react@^19.3.0 react-dom@^19.3.0
```

```tsx
import { QuireMarkdown } from '@quirekit/incremark-react'
import '@quirekit/incremark-react/styles.css'

export function Answer({ received, messageId, receiving }: {
  received: string
  messageId: string
  receiving: boolean
}) {
  return <QuireMarkdown
    content={received}
    identity={messageId}
    status={receiving ? 'receiving' : 'complete'}
    html={{ styles: 'safe' }}
    onError={failure => console.error(failure)}
  />
}
```

`content` is always the complete received source prefix. The host owns network chunks,
cancellation and stopping; the component owns incremental parsing and presentation.
Stopping retains the current string. `status` does not repair markers or freeze earlier
paragraphs. Change `identity` for another message and replace `options` to reconfigure
the parser. Advanced hosts may supply a mutually exclusive `state` snapshot.

## Runnable React example

This example mounts a real React root. The application is `react/app.ts`; the Vue file
only bridges it into this documentation site. Stream again to observe incomplete HTML,
custom markers, late links and footnotes. Disable block reuse for a rendering comparison.

### react/reading.vue

```vue
<script setup lang="ts">
import { onMounted, onBeforeUnmount, shallowRef, watch } from "vue";
import type { ExampleLocale } from "../shared/types";
import type { MarkdownDemoHandle } from "./types";
import { mountMarkdownDemo } from "./mount";
const props = withDefaults(defineProps<{ /** Tutorial language. */ locale?: ExampleLocale }>(), { locale: "zh-CN" });
const parent = shallowRef<HTMLElement>();
let handle: MarkdownDemoHandle | undefined;
onMounted(() => { handle = mountMarkdownDemo(parent.value!, props.locale); });
watch(() => props.locale, locale => handle?.update(locale));
onBeforeUnmount(() => handle?.dispose());
</script>
<template><div ref="parent" class="p-qk-surface" /></template>
<style>
.react-markdown-controls { display: flex; align-items: center; flex-wrap: wrap; gap: .6rem; }
.react-markdown-controls button { font: inherit; padding: .4rem .8rem; border: 1px solid var(--qk-color-border, #dce3ed); background: var(--qk-color-bg, white); border-radius: .5rem; cursor: pointer; }
.react-markdown-controls button:disabled { opacity: .5; cursor: default; }
.react-markdown-controls label { display: flex; align-items: center; gap: .4rem; font-size: .85rem; }
.react-markdown-controls button:focus-visible, .react-markdown-editor textarea:focus-visible { outline: 2px solid var(--qk-color-primary, #2563eb); outline-offset: 2px; }
.react-markdown-status { font-size: .8rem; color: var(--qk-color-text-muted, #64748b); }
.react-markdown-columns { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 1.4rem; }
.react-markdown-editor { display: grid; align-content: start; gap: .5rem; font-size: .85rem; }
.react-markdown-editor textarea { width: 100%; min-height: 530px; resize: vertical; padding: .8rem; font: 12px/1.7 var(--qk-font-code, monospace); border: 1px solid var(--qk-color-border, #dce3ed); border-radius: .6rem; color: inherit; background: var(--qk-color-bg-muted, #f5f7fa); box-sizing: border-box; }
.react-markdown-result { min-width: 0; }
.react-markdown-badge { display: inline-block; padding: 0 .5em; border-radius: .4em; color: #1d4ed8; background: #dbeafe; font-size: .85em; }
@media (max-width: 800px) { .react-markdown-columns { grid-template-columns: minmax(0, 1fr); } .react-markdown-editor textarea { min-height: 180px; } }
</style>

```

### shared/types.ts

```ts
/** Example host language; independent of the documentation router. */
export type ExampleLocale = "zh-CN" | "en";

```

### react/types.ts

```ts
import type { ExampleLocale } from "../shared/types";

/** Documentation host bridge; Markdown source and UI state belong to the React app. */
export interface MarkdownDemoHandle {
  /** @param locale - New tutorial language. */ update(locale: ExampleLocale): void;
  /** Unmounts the React root and releases streaming and reading resources. */ dispose(): void;
}

```

### react/mount.ts

```ts
import { createElement as h, StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { MarkdownDemo } from "./app";
import type { ExampleLocale } from "../shared/types";
import type { MarkdownDemoHandle } from "./types";

/** @param parent - Client-only documentation mount. @param locale - Initial tutorial language.
 * @returns An owned React root; ordinary React applications render MarkdownDemo directly. */
export function mountMarkdownDemo(parent: HTMLElement, locale: ExampleLocale): MarkdownDemoHandle {
  const root = createRoot(parent);
  /** @param next - Tutorial language. */
  function update(next: ExampleLocale): void { root.render(h(StrictMode, null, h(MarkdownDemo, { locale: next }))); }
  update(locale);
  return { update, dispose: () => root.unmount() };
}

```

### react/app.ts

```ts
import { createElement as h, useEffect, useState } from "react";
import type { ReactNode, ChangeEvent } from "react";
import { QuireMarkdown } from "@quirekit/incremark-react";
import type { MarkdownStatus, MarkdownComponents } from "@quirekit/incremark-react";
import "@quirekit/incremark-react/styles.css";
import type { ExampleLocale } from "../shared/types";
import { sample } from "./source";
import { options } from "./extension";

const components: MarkdownComponents = {
  /** @param props - Source metadata and sanitized link inputs. @returns A custom React link; node is consumed before forwarding DOM attributes. */
  a: function Link({ node, children, ...attributes }) { return h("a", { ...attributes, "data-source-type": node.type }, children); },
};

/** @param props - Tutorial locale. @returns A real React-owned streaming example with editable source. */
export function MarkdownDemo(props: { readonly locale: ExampleLocale }): ReactNode {
  const text = props.locale === "en" ? { start: "Stream again", stop: "Stop", full: "Load all", cache: "Reuse blocks", source: "Markdown source", status: "Status", units: "UTF-16 units" }
    : { start: "重新流式生成", stop: "停止", full: "一次性加载", cache: "复用稳定块", source: "Markdown 原文", status: "状态", units: "UTF-16 单元" };
  const [content, setContent] = useState(() => sample(props.locale)), [status, setStatus] = useState<MarkdownStatus>("complete");
  const [identity, setIdentity] = useState(0), [cache, setCache] = useState(true);
  useEffect(() => { setContent(sample(props.locale)); setStatus("complete"); }, [props.locale]);
  // Stage 3: the host owns transport and cancellation; only the received source
  // prefix enters QuireMarkdown. Status never closes or freezes Markdown syntax.
  useEffect(() => {
    if (status !== "receiving") return;
    const source = sample(props.locale); let offset = 0;
    const timer = setInterval(() => {
      offset = Math.min(source.length, offset + 18); setContent(source.slice(0, offset));
      if (offset === source.length) setStatus("complete");
    }, 30);
    return () => clearInterval(timer);
  }, [status, identity, props.locale]);
  /** Starts a new message identity, releasing the previous reader's resources. */
  function start(): void { setIdentity(value => value + 1); setContent(""); setStatus("receiving"); }
  return h("div", { className: "react-markdown-demo" },
    h("div", { className: "react-markdown-controls" },
      h("button", { type: "button", onClick: start }, text.start),
      h("button", { type: "button", onClick: () => setStatus("stopped"), disabled: status !== "receiving" }, text.stop),
      h("button", { type: "button", onClick: () => { setContent(sample(props.locale)); setStatus("complete"); } }, text.full),
      h("label", null, h("input", { type: "checkbox", checked: cache, onChange: event => setCache(event.currentTarget.checked) }), text.cache)),
    h("p", { className: "react-markdown-status", role: "status" }, `${text.status}: ${status} · ${content.length} ${text.units}`),
    h("div", { className: "react-markdown-columns" },
      h("label", { className: "react-markdown-editor" }, text.source, h("textarea", { value: content, spellCheck: false, onChange: (event: ChangeEvent<HTMLTextAreaElement>) => { setStatus("stopped"); setContent(event.currentTarget.value); } })),
      h("div", { className: "react-markdown-result" }, h(QuireMarkdown, { content, status, identity: String(identity), options, components, cache, locale: props.locale, html: { styles: "safe" } }))));
}

```

### react/source.ts

```ts
import type { ExampleLocale } from "../shared/types";

/** @param locale - Tutorial language. @returns A real streaming fixture with custom syntax, HTML and delayed reference definitions. */
export function sample(locale: ExampleLocale): string {
  return locale === "en" ? `## React streaming Markdown

One shared parser, with **React components**. [[stream]]

The [late link][guide] and footnote[^note] become navigable when their definitions arrive.

<div style="padding: 20px; border-left: 4px solid #3b82f6; background: #eff6ff; border-radius: 8px">
<h3>Safe HTML card</h3>
<p>Nested HTML, <strong>emphasis</strong> and resource-free inline styles.</p>
<details><summary>Show details</summary><ul><li>Shared HTML sanitizer</li><li>Source preserved while incomplete</li></ul></details>
</div>

- [x] Custom React components
- [x] Incremental streaming
- [ ] More frameworks

\`\`\`ts
import { QuireMarkdown } from '@quirekit/incremark-react'
\`\`\`

[^note]: A definition arriving after its reference.

[guide]: /en/guide/react
` : `## React 流式 Markdown

同一个解析内核，使用 **React 组件**渲染。[[stream]]

前面的[后置链接][guide]与脚注[^note]会在定义到达后更新。

<div style="padding: 20px; border-left: 4px solid #3b82f6; background: #eff6ff; border-radius: 8px">
<h3>安全 HTML 卡片</h3>
<p>支持嵌套 HTML、<strong>强调</strong>和不加载资源的行内样式。</p>
<details><summary>展开详情</summary><ul><li>共用 HTML 净化规则</li><li>未完整时保留原文</li></ul></details>
</div>

- [x] 自定义 React 组件
- [x] 增量流式更新
- [ ] 更多框架

\`\`\`ts
import { QuireMarkdown } from '@quirekit/incremark-react'
\`\`\`

[^note]: 定义位于正文之后，更新早先的引用。

[guide]: /guide/react
`;
}

```

### react/extension.ts

```ts
import { createElement as h } from "react";
import { Node, defineSource } from "@quirekit/msfe-core";
import { withMarkdownReading } from "@quirekit/incremark-react";
import type { MarkdownOptions } from "@quirekit/incremark-react";

// Stage 1: the source extension owns the grammar and validates persisted fields.
const Badge = Node.create({ name: "react-demo-badge",
  /** @returns Only the documented fixture value is accepted. */
  addAttributes: () => ({ value: { validate: value => value === "stream" } }),
  /** @returns The same canonical source codec used by editing and reading. */
  addSource: () => defineSource<{ value: string }>({ nodes: ["ReactDemoBadge"], syntax: { name: "ReactDemoBadge", open: "[[", close: "]]" },
    /** @param document - Read-only source. @param node - Confirmed complete syntax. @returns Validated candidate fields. */
    read: (document, node) => ({ value: document.slice(node.from + 2, node.to - 2) }), patch: () => null }),
});

// Stage 2: React contributes presentation without replacing the source parser.
export const options: MarkdownOptions = { extensions: [withMarkdownReading(Badge, {
  /** @returns A constant is sufficient because this fixture reads only its own node. */ cacheKey: () => 1,
  /** @param context - Validated immutable inputs. @returns A trusted React badge. */
  render: context => h("span", { className: "react-markdown-badge" }, String(context.attrs.value)),
})] };

```

## Components and extensions

`components` replaces semantic tags with React components or native tags. Custom
components receive `node`, validated properties and `children`. Consume `node` before
forwarding props to a native element; native tags receive no source metadata. Code block
components additionally receive `code` and `language` instead of reconstructing source
from children.

Native Lezer `options.syntax` and MSFE `options.extensions` are shared with Vue. Import
`withMarkdownReading` from the React package to attach ReactNode presentation to the
existing source codec. `renderInline` / `renderBlocks` keep source-bound validation.
`cacheKey(document)` covers external custom dependencies; an omitted key conservatively
reprojects. Do not freeze late definitions to improve cache hit rates.

`load(context, signal)` starts only after client commit. Input replacement, identity
changes and unmount abort it; late resources are also released. A failing `render`
callback preserves source, and node boundaries isolate client descendant failures.
React server rendering does not catch arbitrary descendant exceptions; the host must
handle root SSR failures according to React's error boundary contract.

## SSR, hook and cache

Default `ssr="semantic"` completes the same semantic parse on the server and first
hydration. `ssr="source"` initially escapes source and parses after mount. Both sides
must supply identical content, identity, configuration and locale. Math and diagram
previews use client resources and do not run loaders in SSR.

`useMarkdownSource(input, onError?)` exposes `state`, `pending`, `generation` and `error`
for custom React reading shells. Headless hosts use the [parser core](/en/guide/headless).
`QuireMarkdown` already includes the hook, so it needs no second session.

React prepares a cache frame during render and accepts it after commit. Abandoned
frames neither alter the committed directory nor notify error observers. Advanced
hosts use `MarkdownRenderer.prepare(...)` / `frame.commit()`; synchronous hosts can
use `render(...)`. StrictMode cleanup and reconnection are repeatable. External keys
and HTML callbacks are rechecked on each component render; trigger a React update
after changing their external data.

Core, Vue and React are available; other frameworks are deferred. Reuse covers complete
top-level blocks. See [performance limits](/en/guide/performance) and [React API](/en/api/react).
