@mdx-js/mdx and @mdx-js/react are the two core pillars of the MDX ecosystem, serving distinct but complementary roles in the content rendering pipeline. @mdx-js/mdx is the compiler responsible for transforming Markdown syntax mixed with JSX into valid JavaScript code (usually ES modules) that can be executed by a runtime. It handles parsing, syntax transformation, and plugin integration during the build step. @mdx-js/react, on the other hand, is the runtime library that provides the React components necessary to render the compiled output. It includes the MDXProvider for context management and default mappings for HTML elements to React components, ensuring the compiled JavaScript renders correctly within a React application.
When integrating Markdown with JSX in a React ecosystem, developers often encounter two distinct packages: @mdx-js/mdx and @mdx-js/react. While they share the "MDX" name and work together seamlessly, they solve completely different problems in the software lifecycle. One operates at build time to translate syntax, while the other operates at runtime to render components. Understanding this separation is critical for setting up robust content systems, custom frameworks, or advanced documentation sites.
The fundamental difference lies in their job description. @mdx-js/mdx is a compiler. It takes raw text (Markdown + JSX) and outputs JavaScript code. It does not know about React specifically; it just generates standard ES modules that export a function. @mdx-js/react is a runtime library. It provides the specific React logic needed to take that generated JavaScript function and turn it into visible UI elements in the browser.
@mdx-js/mdx runs during your build process (Node.js environment). It parses the file, applies plugins, and emits code.
// @mdx-js/mdx usage (typically in a build script or loader)
import { compile } from '@mdx-js/mdx';
const mdxContent = '# Hello\n\n<Counter />';
// Compiles raw string into JavaScript code string
const result = await compile(mdxContent, {
outputFormat: 'function-body',
development: false
});
console.log(result.value);
// Output: "function _createMdxContent(props) { ... }"
@mdx-js/react runs in the browser (or server-side rendering context). It supplies the components that the compiled code calls.
// @mdx-js/react usage (in your React App)
import { MDXProvider } from '@mdx-js/react';
import { CompiledMdxComponent } from './generated-file'; // Output from compiler
const components = {
h1: (props) => <h1 style={{ color: 'tomato' }} {...props} />
};
export default function App() {
return (
<MDXProvider components={components}>
<CompiledMdxComponent />
</MDXProvider>
);
}
Configuring how content is processed happens entirely in @mdx-js/mdx. This is where you attach remark plugins (for Markdown analysis) and rehype plugins (for HTML manipulation). If you need to automatically add anchors to headings, transform images, or validate syntax, you do it here. @mdx-js/react has no knowledge of these plugins; it simply receives the final result.
@mdx-js/mdx accepts a rich configuration object to define transformations.
// @mdx-js/mdx: Configuring plugins for build-time transformation
import { compile } from '@mdx-js/mdx';
import remarkGfm from 'remark-gfm';
import rehypeSlug from 'rehype-slug';
await compile(fileContent, {
remarkPlugins: [remarkGfm], // Enables tables, strikethrough, etc.
rehypePlugins: [rehypeSlug], // Adds ID attributes to headings
providerImportSource: '@mdx-js/react' // Tells compiler where to import runtime helpers
});
@mdx-js/react does not accept plugin configurations. Its "configuration" is purely the React Context provided via MDXProvider, which dictates how the final components behave visually.
// @mdx-js/react: No plugin config, only component mapping
import { MDXProvider } from '@mdx-js/react';
// You configure behavior by swapping components, not by passing plugins
const CustomWrapper = ({ children }) => <div className="prose">{children}</div>;
export function Layout({ children }) {
return (
<MDXProvider components={{ wrapper: CustomWrapper }}>
{children}
</MDXProvider>
);
}
To visualize the data flow, imagine a pipeline. The raw .mdx file enters @mdx-js/mdx. The package outputs a .js file. This .js file is then bundled by your tool (Vite, Webpack, etc.) and imported into your application. Once imported, the code executes and relies on @mdx-js/react to resolve the JSX tags.
If you try to use @mdx-js/mdx in the browser to compile content on the fly, you will face significant performance penalties and large bundle sizes because you are shipping a parser to the client. Conversely, if you try to render compiled MDX without @mdx-js/react, you will get errors about missing components like MDXProvider or useMDXComponents.
@mdx-js/mdx generates code that explicitly imports helpers.
// Output generated by @mdx-js/mdx
import { jsx as _jsx } from 'react/jsx-runtime';
import { useMDXComponents as _provideComponents } from '@mdx-js/react'; // Runtime dependency
export function Content(props) {
const _components = _provideComponents();
return _jsx(_components.h1, { children: "Hello World" });
}
@mdx-js/react exports the hooks and providers that satisfy those imports.
// Internal logic simplified from @mdx-js/react
import { useContext, createContext } from 'react';
const MDXContext = createContext({});
export function useMDXComponents(components) {
// Logic to merge user components with defaults
return { ...useContext(MDXContext), ...components };
}
export function MDXProvider({ components, children }) {
return <MDXContext.Provider value={components}>{children}</MDXContext.Provider>;
}
Your choice of environment dictates which package is active. @mdx-js/mdx is strictly a Node.js tool. It relies on file system access and heavy parsing libraries that are unsuitable for client-side execution in production. @mdx-js/react is designed for the frontend (React DOM, React Native, or Server Components) and must be included in your final bundle.
@mdx-js/mdx should never be imported in a client-side component unless you are building a specific "live editor" tool and accept the performance cost.
// ❌ Bad Practice: Importing compiler in a client component
import { compile } from '@mdx-js/mdx';
// This bloats your bundle with parser logic unnecessary for rendering
// ✅ Good Practice: Using the compiler in a build script (build.mjs)
import { compile } from '@mdx-js/mdx';
// Runs once during deployment to generate static JS files
@mdx-js/react is safe and required for client-side rendering.
// ✅ Correct: Importing runtime in components
import { MDXProvider } from '@mdx-js/react';
export default function BlogPost({ content: Content }) {
return (
<article>
<MDXProvider>
<Content />
</MDXProvider>
</article>
);
}
Despite their different roles, both packages are maintained by the same core team and follow strict semantic versioning together. They both aim to decouple content authoring from presentation logic.
// package.json
{
"dependencies": {
"@mdx-js/mdx": "^3.0.0",
"@mdx-js/react": "^3.0.0" // Must match major version
}
}
// Compiler setting the stage
await compile(content, { providerImportSource: '@mdx-js/react' });
// Runtime delivering the actors
<MDXProvider components={{ p: ParagraphComponent }} />
// Both support modern import/export syntax
import { compile } from '@mdx-js/mdx'; // ESM
import { MDXProvider } from '@mdx-js/react'; // ESM
| Feature | @mdx-js/mdx | @mdx-js/react |
|---|---|---|
| Primary Role | Compiler (Text → JS) | Runtime (JS → UI) |
| Execution Env | Node.js (Build Time) | Browser/Server (Run Time) |
| Dependencies | Remark, Rehype, Acorn | React, React-JSX-Runtime |
| Output | JavaScript Code String | React Components/Context |
| Configuration | Plugins (Remark/Rehype) | Component Mapping (Props) |
| Bundle Impact | Dev Dependency (Not shipped) | Prod Dependency (Shipped) |
Think of @mdx-js/mdx as the translator. It takes the author's mixed language of Markdown and JSX and translates it into pure JavaScript that the computer can execute. You interact with it when you set up your build pipeline, configure Vite/Webpack loaders, or write scripts to generate pages.
Think of @mdx-js/react as the actor. It takes the script produced by the translator and performs it on the stage (the DOM). You interact with it in your React components to ensure the content looks right, applies your theme, and handles interactive elements.
Final Thought: In 95% of projects using frameworks like Next.js or Gatsby, you won't import @mdx-js/mdx directly because the framework handles the compilation step for you. However, you will almost always need @mdx-js/react in your package.json and your code to render the content. If you are building a custom engine or a live playground, you will need to orchestrate both: using the compiler to generate code on the server and the runtime to display it on the client.
Choose @mdx-js/react if you are developing a React application that needs to display MDX content. This package is mandatory in your frontend dependencies to provide the MDXProvider and the internal components that map standard HTML tags (like h1, p, img) to React components. You need this library whenever you import compiled MDX files or use the useMDXComponents hook to customize how content is rendered, regardless of whether the compilation happened via Vite, Next.js, or a custom script.
Choose @mdx-js/mdx if you are building a build tool, a framework plugin (like for Next.js, Vite, or Webpack), or a custom CLI that needs to convert .mdx files into JavaScript before the application runs. This package is essential when you need to process content at compile-time to optimize performance, enable static site generation (SSG), or apply complex transformations via remark/rehype plugins. It is not used directly in the browser or for dynamic runtime parsing without a build step.
@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.