@mdx-js/mdx 和 @mdx-js/react 是 MDX 生态系统的两个核心底层包,它们共同解决了在 Markdown 中嵌入 JSX 并在 React 应用中渲染的问题,但分工明确。@mdx-js/mdx 是一个编译器,负责将 .mdx 文件内容转换为标准的 JavaScript 代码(通常是 ESM 模块),它不依赖 React 运行时,可以在 Node.js 构建步骤或任何 JavaScript 环境中运行。@mdx-js/react 则是一个运行时库,提供了在 React 中渲染编译后 MDX 内容所需的核心组件(如 MDXProvider 和 useMDXComponents)以及处理作用域的逻辑。简单来说,前者负责“翻译”文件,后者负责“执行”和“展示”翻译后的内容。在现代前端架构中,通常由构建工具(如 Vite, Next.js, Webpack 插件)在后台调用 @mdx-js/mdx 进行预编译,而开发者则在代码中直接引入 @mdx-js/react 来动态渲染内容或提供全局组件上下文。
在构建包含 Markdown 和 JSX 混合内容的应用时,许多开发者容易混淆 @mdx-js/mdx 和 @mdx-js/react 的用途。事实上,它们并非竞争关系,而是 MDX 工作流中两个截然不同阶段的基石:一个是编译器(Compiler),一个是运行时(Runtime)。理解它们的边界对于设计灵活的内容系统至关重要。
@mdx-js/mdx 是一个纯粹的转换器。它的任务是将人类可读的 .mdx 文本转换成 JavaScript 代码。它不知道 React 是什么,也不关心浏览器环境。它只关心语法解析和代码生成。
// 使用 @mdx-js/mdx 进行编译
import { compile } from '@mdx-js/mdx';
const mdxContent = '# Hello\n\n<Component prop="value" />';
async function transform() {
// 输出的是 JavaScript 代码字符串,而非 UI
const result = await compile(mdxContent, {
outputFormat: 'function-body',
development: false
});
console.log(String(result));
// 输出类似: "function MDXContent(props = {}) { ... }"
}
@mdx-js/react 则是为了让上述生成的代码能在 React 中跑起来而存在的。它提供了必要的上下文环境和渲染原语。如果没有它,编译后的代码在 React 中无法正确解析自定义组件或处理作用域。
// 使用 @mdx-js/react 进行渲染
import { MDXProvider, useMDXComponents } from '@mdx-js/react';
const components = {
h1: (props) => <h1 style={{ color: 'tomato' }} {...props} />
};
function App() {
return (
// 提供全局组件映射,让编译后的 MDX 内容能找到这些组件
<MDXProvider components={components}>
<MDXContent /> {/* 这是由 @mdx-js/mdx 编译生成的组件 */}
</MDXProvider>
);
}
在实际工程中,这两个包通常出现在不同的阶段。
@mdx-js/mdx 主要活跃在构建时(Build Time)。当你使用 Vite、Webpack 或 Next.js 插件时,这些工具会在后台调用 @mdx-js/mdx 把文件系统中的 .mdx 文件预编译成 .js 文件。你很少直接在业务代码中导入它,除非你在写底层工具。
// 场景:编写一个自定义的 Node.js 脚本,批量预处理 MDX 文件
import { compile } from '@mdx-js/mdx';
import fs from 'fs';
async function prebuild() {
const file = fs.readFileSync('docs/intro.mdx', 'utf8');
const { code } = await compile(file);
fs.writeFileSync('dist/intro.js', code);
// 这里完全不需要 React 环境
}
@mdx-js/react 则活跃在运行时(Runtime),特别是当你需要动态内容时。例如,从 Headless CMS(如 Contentful 或 Sanity)拉取一段 MDX 字符串,然后在客户端即时渲染。这种情况下,没有文件系统的预编译过程,你需要手动调用编译器(或在服务端预编译好),然后在客户端用 @mdx-js/react 来挂载它。
// 场景:在客户端动态渲染来自 API 的 MDX 字符串
import { evaluate } from 'mdx-content-evaluator'; // 假设有一个评估器
import { MDXProvider } from '@mdx-js/react';
async function DynamicPage({ mdxString }) {
// 在实际生产中,通常需要在服务端先 compile,然后传输 JS
// 这里演示运行时依赖关系
const Content = await evaluate(mdxString, {
// 必须传入 React 相关的运行时辅助
react: { createElement: React.createElement, Fragment: React.Fragment },
useMDXComponents: useMDXComponents
});
return (
<MDXProvider>
<Content />
</MDXProvider>
);
}
处理自定义组件(比如把普通的 <a> 标签替换为框架级的 <Link>)是 MDX 的核心需求。这两个包在此处的角色非常清晰。
@mdx-js/mdx 在编译阶段,会将 Markdown 中的标签转换为 useMDXComponents() 的调用。它生成的代码长这样:
// @mdx-js/mdx 生成的代码片段
function MDXContent(props = {}) {
const { wrapper: MDXLayout } = {
...useMDXComponents(), // 这里调用了 @mdx-js/react 提供的 hook
...props.components
};
return MDXLayout ? (
<MDXLayout><h1>{"Hello"}</h1></MDXLayout>
) : (
<><h1>{"Hello"}</h1></>
);
}
注意,编译器只是生成了调用 useMDXComponents 的代码,它并不实现这个 hook。
@mdx-js/react 实现了 useMDXComponents 和 MDXProvider。它维护了一个 React Context,允许你在组件树的顶层定义一套组件规则,并让深层的编译内容自动继承。
// @mdx-js/react 提供的能力
import { MDXProvider } from '@mdx-js/react';
import { Link } from 'next/link';
// 显式定义组件映射
const customComponents = {
a: (props) => <Link {...props} />
};
function Layout({ children }) {
return (
// 所有子级 MDX 内容中的 <a> 都会自动变成 <Link>
<MDXProvider components={customComponents}>
{children}
</MDXProvider>
);
}
由于职责分离,这两个包对最终打包的影响也不同。
@mdx-js/mdx 不会出现在最终的用户浏览器包中(除非你做极其特殊的运行时编译)。它是一个开发依赖(devDependency),只在构建服务器或本地开发环境中运行。它的性能影响体现在构建速度上。如果项目中有数百个 MDX 文件,@mdx-js/mdx 的编译效率直接决定了 CI/CD 的耗时。
@mdx-js/react 是生产依赖(dependency)。它会被打入最终的 JavaScript 包中发送给浏览器。不过,它的体积非常小,主要包含 Context 创建和一些辅助逻辑。在大多数情况下,它的开销可以忽略不计,但它是运行时渲染动态内容的必要成本。
// package.json 依赖示例
{
"devDependencies": {
// 仅在构建时需要
"@mdx-js/mdx": "^3.0.0"
},
"dependencies": {
// 运行时需要,如果涉及动态渲染或使用 Provider
"@mdx-js/react": "^3.0.0"
}
}
适用场景:文档站、博客
@mdx-js/mdx: 由构建插件(如 @mdx-js/loader)调用,将 .mdx 转为 .js。@mdx-js/react: 通常不需要显式引入,除非你需要自定义全局组件。许多框架(如 Next.js 配合 @next/mdx)会自动处理这部分运行时注入。适用场景:用户生成的内容、CMS 驱动页面
@mdx-js/mdx: 可能在 Node.js 服务端按需调用,将 CMS 返回的字符串编译成函数。@mdx-js/react: 必须。客户端需要它来渲染编译后的组件树,并提供组件上下文。// 动态渲染简例
import { compile } from '@mdx-js/mdx'; // 服务端使用
import { MDXProvider } from '@mdx-js/react'; // 客户端使用
// 服务端:const code = await compile(content);
// 客户端:<MDXProvider><CompiledComponent /></MDXProvider>
| 特性 | @mdx-js/mdx | @mdx-js/react |
|---|---|---|
| 角色 | 编译器 (Compiler) | 运行时 (Runtime) |
| 输入 | Markdown/MDX 字符串 | 编译后的 JS 组件 / React 元素 |
| 输出 | JavaScript 代码字符串 | 渲染后的 React UI |
| 运行环境 | Node.js (构建时) | 浏览器/Node (运行时) |
| 主要 API | compile(), recmaPlugins | MDXProvider, useMDXComponents |
| 依赖类型 | devDependencies | dependencies |
| 典型用户 | 框架作者、构建工具开发者 | 应用开发者、动态内容需求者 |
对于大多数应用开发者:
.mdx 文件并通过 Vite/Next.js 导入,你实际上已经在间接使用 @mdx-js/mdx 了(通过插件)。你通常只需要关注是否需要安装 @mdx-js/react 来使用 MDXProvider 定制组件样式。@mdx-js/mdx 编译,在客户端用 @mdx-js/react 渲染。@mdx-js/mdx 进行即时编译,这会带来巨大的性能惩罚和安全风险。始终遵循“编译在边缘/服务端,渲染在客户端”的原则。这两个包共同构成了 MDX 的完整生命周期:@mdx-js/mdx 负责将内容变成代码,@mdx-js/react 负责让代码变成界面。理解这一分工,能让你更从容地应对从静态文档到动态内容平台的各类挑战。
当你在 React 应用中需要动态渲染 MDX 内容(例如从 CMS 获取字符串后渲染),或者需要自定义全局 JSX 组件映射(如统一处理所有的 h1 或 a 标签)时,应选择 @mdx-js/react。它是运行时依赖,提供了 MDXProvider 和渲染逻辑。如果你的项目只是使用现成的构建工具(如 @next/mdx),通常不需要直接安装它,但在动态渲染场景下它是必不可少的。
当你需要构建自定义的构建插件、编写脚本批量转换 MDX 文件,或者在非 React 环境(如纯 Node.js 服务)中将 MDX 转换为 JavaScript 字符串时,应选择 @mdx-js/mdx。它是底层编译工具,不负责渲染,只负责生成代码。如果你正在开发一个类似 Next.js 或 Gatsby 的框架插件,这是你必须直接交互的包。
@mdx-js/reactReact context for MDX.
This package is a context based components provider for combining React with MDX.
This package is not needed for MDX to work with React. See ¶ MDX provider in § Using MDX for when and how to use an MDX provider.
If you use Next.js, do not use this.
Add an mdx-components.tsx (in src/ or /) file instead.
See Configuring MDX on nextjs.org for more info.
This package is ESM only. In Node.js (version 16+), install with npm:
npm install @mdx-js/react
In Deno with esm.sh:
import {MDXProvider} from 'https://esm.sh/@mdx-js/react@3'
In browsers with esm.sh:
<script type="module">
import {MDXProvider} from 'https://esm.sh/@mdx-js/react@3?bundle'
</script>
/**
* @import {MDXComponents} from 'mdx/types.js'
*/
import {MDXProvider} from '@mdx-js/react'
import Post from './post.mdx'
// ^-- Assumes an integration is used to compile MDX to JS, such as
// `@mdx-js/esbuild`, `@mdx-js/loader`, `@mdx-js/node-loader`, or
// `@mdx-js/rollup`, and that it is configured with
// `options.providerImportSource: '@mdx-js/react'`.
/** @type {MDXComponents} */
const components = {
em(properties) {
return <i {...properties} />
}
}
console.log(
<MDXProvider components={components}>
<Post />
</MDXProvider>
)
👉 Note: you don’t have to use
MDXProviderand can pass components directly:-<MDXProvider components={components}> - <Post /> -</MDXProvider> +<Post components={components} />
See ¶ React in § Getting started for how to get started with MDX and React. See ¶ MDX provider in § Using MDX for how to use an MDX provider.
This package exports the identifiers MDXProvider and
useMDXComponents.
There is no default export.
MDXProvider(properties?)Provider for MDX context.
properties (Props)
— configurationElement (ReactElement).
useMDXComponents(components?)Get current components from the MDX Context.
components (MDXComponents from mdx/types.js
or MergeComponents, optional)
— additional components to use or a function that creates themCurrent components (MDXComponents from
mdx/types.js).
MergeComponentsCustom merge function (TypeScript type).
components (MDXComponents from mdx/types.js)
— current components from the contextAdditional components (MDXComponents from
mdx/types.js).
PropsConfiguration for MDXProvider (TypeScript type).
children (ReactNode from react,
optional)
— childrencomponents (MDXComponents from mdx/types.js
or MergeComponents, optional)
— additional components to use or a function that creates themdisableParentContext (boolean, default: false)
— turn off outer component contextThis package is fully typed with TypeScript.
It exports the additional types MergeComponents and
Props.
For types to work, make sure the TypeScript JSX namespace is typed.
This is done by installing and using the types of your framework, as in
@types/react.
Projects maintained by the unified collective are compatible with maintained versions of Node.js.
When we cut a new major release, we drop support for unmaintained versions of
Node.
This means we try to keep the current release line, @mdx-js/react@^3,
compatible with Node.js 16.
See § Security on our website for information.
See § Contribute on our website for ways to get started. See § Support for ways to get help.
This project has a code of conduct. By interacting with this repository, organization, or community you agree to abide by its terms.