Skip to navigation
2-3 minutes read
By Titus Wormer

Injecting components

This guide shows how to inject arbitrary components into MDX when it runs. It shows how the underlying features used by our providers (@mdx-js/react, @mdx-js/preact) and the mdx-components.tsx file supported by Next.js work, and how you can take advantage of that functionality yourself.

In many cases you do not need this, as you can pass components to MDX:

example.mdx
# Hello *<Planet />*

You can pass Planet and say a component used instead of the h1:

example.jsx
import Example from './example.mdx' // Assumes an integration is used to compile MDX -> JS.

<Example
  components={{
    Planet() {
      return 'Pluto'
    },
    h1(properties) {
      return <h2 {...properties} />
    }
  }}
/>

When you find yourself passing that components prop around a lot, you might want to look at an alternative. You might reach for our context based providers (@mdx-js/react, @mdx-js/preact), but context has performance downsides and context doesn’t always work (such as in RSC).

But first, how does component passing work? That can be illustrated by looking at the code generated by MDX for the above example.mdx. Here is a diff that shows what the example normally compiles to and what changes when providerImportSource: 'xxx' is passed:

Diff
@@ -1,7 +1,13 @@
 import {jsx as _jsx, jsxs as _jsxs} from 'react/jsx-runtime'
+import {useMDXComponents as _provideComponents} from 'xxx'

 function _createMdxContent(props) {
-  const _components = {em: 'em', h1: 'h1', ...props.components}
+  const _components = {
+    em: 'em',
+    h1: 'h1',
+    ..._provideComponents(),
+    ...props.components
+  }
   const {Planet} = _components
   if (!Planet) _missingMdxReference('Planet', true)
   return _jsxs(_components.h1, {
@@ -10,7 +16,7 @@ function _createMdxContent(props) {
 }

 export default function MDXContent(props = {}) {
-  const {wrapper: MDXLayout} = props.components || {}
+  const {wrapper: MDXLayout} = {..._provideComponents(), ...props.components}
   return MDXLayout
     ? _jsx(MDXLayout, {...props, children: _jsx(_createMdxContent, {...props})})
     : _createMdxContent(props)

Observe that components have defaults (such as that h1 will use 'h1') and that components are taken from props.components. What changes is an added call to _provideComponents, which refers to an useMDXComponents export from the module we specified (xxx).

We can use this interface to inject components from a file. In that file, we need a useMDXComponents function that returns our components.

mdx-components.js
/** @returns {import('mdx/types.js').MDXComponents} */
export function useMDXComponents() {
  return {
    Planet() {
      return 'Pluto'
    },
    h1(properties) {
      return <h2 {...properties} />
    }
  }
}

And now passing a file path or URL to that file as providerImportSource, such as with import.meta.resolve('./mdx-components.js'):

Diff
@@ -1,5 +1,5 @@
 import {jsx as _jsx, jsxs as _jsxs} from 'react/jsx-runtime'
-import {useMDXComponents as _provideComponents} from 'xxx'
+import {useMDXComponents as _provideComponents} from 'file:///Users/tilde/…/mdx-components.js'

Now our locally defined components will be used in all MDX files!