# React 接入

React 组件、流式更新、SSR 和自定义节点，与 Vue 共用解析内核。

`@quirekit/incremark-react` 提供 React 19 的 `QuireMarkdown`，直接依赖
`@quirekit/incremark-core`。解析、后置引用、脚注、HTML 净化和缓存规则与 Vue 一致。
React 包使用自己的语义 DOM，不需要 Vue、QuireKit UI 或 UnoCSS。

## 工作区接入

当前尚未发布到 npm；在 QuireKit 工作区的 React 应用中安装：

```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` 始终是已收到的权威全文。宿主负责网络分片、取消与停止，组件负责增量解析
和呈现。停止保留当前字符串；`status` 不修补标记或冻结旧段落。更换消息时改变
`identity`；修改 parser 配置时替换 `options` 对象。高级宿主可传互斥的 `state` 快照。

## 可运行的 React 示例

下面挂载真实 React root；运行代码位于 `react/app.ts`，Vue 文件只是本站的挂载桥。
重新生成时观察未完整 HTML、业务标记、后置链接和脚注；复选框可关闭渲染缓存作对照。

### 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` 按语义标签替换 React 组件或原生标签。自定义组件获得 `node`、已验证属性
和 `children`；向原生元素转发 props 时先取出 `node`。原生标签不接收该元数据。
代码块组件另外获得 `code`、`language`，不需要从 children 反推代码原文。

原生 Lezer `options.syntax` 和 MSFE `options.extensions` 与 Vue 共用；从 React 包导入
`withMarkdownReading` 为已有 source codec 安装返回 ReactNode 的阅读贡献。
`renderInline` / `renderBlocks` 沿用源码范围校验；`cacheKey(document)` 覆盖自定义读取的
外部依赖。缺省 key 保守重投影，不能为了缓存把后置定义提前冻结。

`load(context, signal)` 只在客户端提交后启动；更换输入、identity 或卸载会 abort，
迟到资源也释放。自己的 `render` 回调失败保留原文，客户端的子组件错误由节点边界隔离。
React 服务端渲染不捕获任意子组件抛错，宿主仍须处理 SSR 根级错误；这一点遵循 React 的边界。

## SSR、hook 与缓存

默认 `ssr="semantic"` 在服务端和首次水合完成相同语义解析；`ssr="source"` 先输出转义
原文，再在挂载后解析。两端必须传入相同内容、identity、配置与 locale。公式和图形
预览沿用客户端资源渲染，不在 SSR 执行 loader。

`useMarkdownSource(input, onError?)` 供自己的 React 阅读外壳消费 `state`、`pending`、
`generation` 和 `error`；无 UI 宿主使用[纯解析内核](/guide/headless)。
`QuireMarkdown` 已内置 hook，不需要再为它维护第二个会话。

React 在 render 中准备缓存帧，提交后接受；被放弃的帧不更改已提交目录，也不通知错误 observer。
高级宿主用 `MarkdownRenderer.prepare(...)` / `frame.commit()` 接入这一合同；同步宿主
可使用 `render(...)`。StrictMode 的清理与重新连接可以安全重复。
依赖 key 和 HTML 回调每次组件 render 重新检查，宿主改变外部数据后须触发 React 更新。

当前交付 core、Vue 和 React；其他框架暂不实现。缓存以完整顶层块为单位，成本边界见
[性能说明](/guide/performance)，逐项类型见 [React API](/api/react)。
