Skip to navigation
3-5 minutes read
By Chris Biscardi

Note: This is an old blog post. The steps described in it are no longer performed by MDX. It now uses an JavaScript AST first rather than generating a string of JSX. It is also no longer required for extra glue code, that combines MDX with a specific framework, to be used on the client. See § How MDX works and ¶ Architecture for more info. The below is kept as is for historical purposes.

Custom pragma

MDXTag, for those that aren’t aware, is a critical piece in the way MDX replaces HTML primitives like <pre> and <h1> with custom React Components. I’ve previously written about the way MDXTag works when trying to replace the <pre> tag with a custom code component. mdx-utils contains the methodology for pulling the props around appropriately through the MDXTag elements that are inbetween pre and code.

TypeScript
exports.preToCodeBlock = preProps => {
  if (
    // children is MDXTag
    preProps.children &&
    // MDXTag props
    preProps.children.props &&
    // if MDXTag is going to render a <code>
    preProps.children.props.name === 'code'
  ) {
    // we have a <pre><code> situation
    const {
      children: codeString,
      props: {className, ...props}
    } = preProps.children.props

    return {
      codeString: codeString.trim(),
      language: className && className.split('-')[1],
      ...props
    }
  }
  return undefined
}

So MDXTag is a real Component in the middle of all of the other MDX rendered elements. All of the code is included here for reference.

TypeScript
import React, {Component} from 'react'

import {withMDXComponents} from './mdx-provider'

const defaults = {
  inlineCode: 'code',
  wrapper: 'div'
}

class MDXTag extends Component {
  render() {
    const {
      name,
      parentName,
      props: childProps = {},
      children,
      components = {},
      Layout,
      layoutProps
    } = this.props

    const Component =
      components[`${parentName}.${name}`] ||
      components[name] ||
      defaults[name] ||
      name

    if (Layout) {
      return (
        <Layout components={components} {...layoutProps}>
          <Component {...childProps}>{children}</Component>
        </Layout>
      )
    }

    return <Component {...childProps}>{children}</Component>
  }
}

export default withMDXComponents(MDXTag)

MDXTag is used in the hast to estree conversion, which is the final step in the MDX AST pipeline. Every renderable element is wrapped in an MDXTag, and MDXTag handles rendering the element later.

TypeScript
return `<MDXTag name="${node.tagName}" components={components}${
  parentNode.tagName ? ` parentName="${parentNode.tagName}"` : ''
}${props ? ` props={${props}}` : ''}>${children}</MDXTag>`

A concrete example

The following MDX

MDX
# a title

    and such

testing

turns into the following React code

TypeScript
export default ({components, ...props}) => (
  <MDXTag name="wrapper" components={components}>
    <MDXTag name="h1" components={components}>{`a title`}</MDXTag>{' '}
    <MDXTag name="pre" components={components}>
      <MDXTag
        name="code"
        components={components}
        parentName="pre"
        props={{metaString: null}}
      >{`and such `}</MDXTag>
    </MDXTag>{' '}
    <MDXTag name="p" components={components}>{`testing`}</MDXTag>
  </MDXTag>
)

resulting in the following HTML

HTML
<div>
  <h1>a title</h1>
  <pre>
    <code>and such</code>
  </pre>
  <p>testing</p>
</div>

createElement

With the new approach, the above MDX transforms into this new React code

TypeScript
const layoutProps = {}
export default function MDXContent({components, ...props}) {
  return (
    <MDXLayout
      {...layoutProps}
      {...props}
      components={components}
      mdxType="MDXLayout"
    >
      <h1>{`a title`}</h1>
      <pre>
        <code parentName="pre" {...{}}>{`and such
`}</code>
      </pre>
      <p>{`testing`}</p>
    </MDXLayout>
  )
}

MDXContent.isMDXComponent = true

Notice how now the React elements are plainly readable without wrapping MDXTag.

Now that we’ve cleaned up the intermediary representation, we need to make sure that we have the same functionality as the old MDXTag. This is done through a custom createElement implementation. Typically when using React, we use React.createElement to render the elements on screen. This is even true if you’re using JSX because JSX tags such as <div> compile to createElement calls. So this time instead of using React.createElement we’ll be using our own.

Reproduced here is our createElement function and the logic for how we decide whether or not MDX should take over the rendering of the createElement call.

TypeScript
export default function (type, props) {
  const args = arguments
  const mdxType = props && props.mdxType

  if (typeof type === 'string' || mdxType) {
    const argsLength = args.length

    const createElementArgArray = new Array(argsLength)
    createElementArgArray[0] = MDXCreateElement

    const newProps = {}
    for (let key in props) {
      if (hasOwnProperty.call(props, key)) {
        newProps[key] = props[key]
      }
    }
    newProps.originalType = type
    newProps[TYPE_PROP_NAME] = typeof type === 'string' ? type : mdxType

    createElementArgArray[1] = newProps

    for (let i = 2; i < argsLength; i++) {
      createElementArgArray[i] = args[i]
    }

    return React.createElement.apply(null, createElementArgArray)
  }

  return React.createElement.apply(null, args)
}

Vue

One really cool application of the new output format using a custom createElement is that we can now write versions of it for Vue and other frameworks. Since the pragma insertion is the responsibility of the webpack (or other bundlers) loader, swapping the pragma can be an option in mdx-loader as long as we have a Vue createElement to point to.