Wilson Staley

Replacing MDX Code Blocks with a Custom Component

December 31st, 2020

In my last post, I made use of a lot of code blocks. This is definitely somthing I'll want to do in future posts as well! The problem with the default code block in markdown is that they are not syntax highlighted and it would also be nice to have the feature where you can copy the code directly to your clipboard.

Here's how I replaced the default code blocks with my own React component...

Step 1 - Implement an MDXProvider

The MDX-JS React package comes with a nifty provider that you can use to swap out markdown elements with a component of your choosing. If you're interested, you can read more about it here.

Let me show you the code and then explain how it works:

//gatsby-browser.js
import React from "react"
import { MDXProvider } from "@mdx-js/react"
import CodeSnippet from "./src/components/CodeSnippet/CodeSnippet"

const components = {
  code: props => <CodeSnippet {...props} />,
}

export const wrapRootElement = ({ element }) => {
  return <MDXProvider components={components}>{element}</MDXProvider>
}

The first thing to note is that I am making use of Gatsby's wrapRootElement hook to wrap the root element of my site in the MDXProvider. You can use this hook whenever you want to make use of a provider component like Redux for example.

The MDXProvider takes a components prop. This prop is an object that maps mdx elements to custom components. So here I am basically saying, anytime you see a code block in mdx, render my custom CodeSnippet component instead. The contents of the mdx code block will be passed to my CodeSnippet component as the children prop.

Step 2 - Create a Code Block Component

So what's in this CodeSnippet component?

I decided to use the react-code-blocks package to easily get a syntax-highlighted code block component. This package also provides a copyblock that includes a button to copy the code to the user's clipboard.

Here's what the initial component looks like:

import React from "react"
import { CodeBlock, anOldHope } from "react-code-blocks"

const CodeSnippet = ({ children }) => {
  return (
    <CodeBlock
      text={children}
      language='javascript'
      showLineNumbers={false}
      theme={anOldHope}
    />
  )
}

export default CodeSnippet

This is pretty straightforward... I'm importing the CodeBlock component and the 'anOldHope' theme from the react-code-blocks package. Then I'm just returning an instance of the CodeBlock component with the text that is passed in via the children prop.

This is honestly a pretty complete solution, but a couple things need to be fixed for my site.

  1. The language should not be hardcoded as 'javascript'
  2. The code block's theme should change depending on whether or not dark mode is enabled.

Step 3 - Make the Language Dynamic

Whenver I create a code block in mdx, I can specify what language it is next to the triple backticks like so:

```jsx
some code here...
```

Then I can grab this from inside the CodeSnippet component via the className prop:

import React from "react"
import { CodeBlock, anOldHope } from "react-code-blocks"

const CodeSnippet = ({ children, className }) => {
  //Get the code's language from the mdx fence info
  const language = className.replace(/language-/, "")

  return (
    <CodeBlock
      text={children}
      language={language}
      showLineNumbers={false}
      theme={anOldHope}
    />
  )
}

export default CodeSnippet

You can find a list of supported languages in the react-code-blocks library here

Step 4 - Make the Theme Dynamic

Right now I am just using the 'anOldHope' theme for all of the code blocks, but it would be nice if they had a dark theme when dark mode is enabled on the site and a light theme when it is not.

In order to accomplish this, I decided to have the CodeSnippet component listen for the body's class name to change. The body's classname is changed to 'dark' or 'light' whenever the user toggles dark mode. I am not sure this is the most elegant solution, but it works for what I'm doing 🙃

Here's what the CodeSnippet component looks like after making these changes:

import React, { useState } from "react"
import { CodeBlock, anOldHope, googlecode } from "react-code-blocks"
import { isDarkMode } from "../../utils"

const CodeSnippet = ({ children, className }) => {
  //Get the code's language from the mdx fence info
  const language = className.replace(/language-/, "")
  //Adjust the provided code block theme
  googlecode.backgroundColor = "hsla(0, 0%, 0%, 0.04)"
  const [darkMode, setDarkMode] = useState(isDarkMode())

  //Listen for the body element's className to change ('dark' or 'light').
  //We need the theme of the site to determine which code block theme to use!
  const targetNode = document.getElementsByTagName("BODY")[0]
  const config = { attributes: true }
  const callback = function (mutationsList, observer) {
    for (const mutation of mutationsList) {
      setDarkMode(mutation.target.className === "dark" ? true : false)
    }
  }
  const observer = new MutationObserver(callback)
  observer.observe(targetNode, config)

  return (
    <CodeBlock
      text={children}
      language={language}
      showLineNumbers={false}
      theme={darkMode ? anOldHope : googlecode}
    />
  )
}

export default CodeSnippet

This works by adding a darkMode boolean that is initially set to the current state of the site's dark mode. Then I am using a MutationObserver to listen for a change of the body element's class. When the class changes, I set the state of darkMode to true or false depending on the whether the class name was 'dark' or 'light'. Finally, depending on the value of darkMode, I set the theme of the code block to either 'anOldHope' or 'googlecode'. I simply chose these themes based on what I think looks good.

There a few things that could be done to make this better. For example I could disconnect the MutationObserver once the component dismounts. It might also be helpful to make the callback such that it will only ever call setDarkMode once. It is fine for now though because I know that I am always observing exactly one node - the body.

And there it is! That is how I replaced mdx code blocks with a custom React component in my Gatsby site. The new code blocks are syntax highlighted, could easily be modified to have a "copy to clipboard" button, and change themes to match the current theme of the site.

Created by Wilson Staley, © 2021