heroPhoto by Laurentiu Iordache on Unsplash

How to render react components in markdown in Gatsby

In a recent post Stop trying, you can't multitask, I wanted to embed a few productivity books which I found useful. Since I write all the content in Markdown, I thought about embedding iframe components or insert image version of the book components in the Markdown, which are both terrible ideas. Luckily, there's a better way to do this with MDX, which allowed me to use React components inside Markdown. The solution is super clean, and in this article, I'll be explaining how to add React components to your Gatsby site.

Install Gatsby plugins

There are two things we need to get this to work:

  1. Gatsby needs to understand the React component when parsing the Markdown document. I used gatsby-remark-component-parent2div to detect custom components.
  2. Gatsby needs to hydrate the React component. rehype-react is required to transform HTML to React component.

Now install the following dependencies:

npm i gatsby-transformer-remark rehype-react gatsby-remark-component-parent2div

Why I use gatsby-remark-component-parent2div

Initially, I tried using gatsby-remark-component plugin. However, I found that it was throwing warnings in the console. It turns out Contentful, the headless CMS I am using, returns <div> inside <p>. It is against HTML specification so that some browsers will throw the warning. There are two ways to resolve this:

  • Change the component, avoid using <div> inside <p>
  • Use gatsby-remark-component-parent2div plugin instead, which changes the AST node parent of your custom component from <p> to <div>.

The first method doesn't work for me since I cannot control how Contentful save Markdown into HTML. In the end, I went with the second method, which arguably is the easier approach out of the two.

If you don't have this issue, I suggest you use use gatsby-remark-component plugin instead.

Update gatsby-config.js

If you want the ability to auto-detect custom components, then the config update is minimal.

plugins: [{
    resolve: "gatsby-transformer-remark",
    options: {
        plugins: ["gatsby-remark-component-parent2div"]
    }
}]

I wanted to be somewhat defensive with my approach, incase in the future, there's a use case for non-React custom components.

plugins: [{
    resolve: "gatsby-transformer-remark",
    options: {
        plugins: [{
            resolve: "gatsby-remark-component-parent2div",
            options: {
                components: ["book-affiliate-link"],
                verbose: true
            }
        }]
    }
}]

Update Markdown template

This step will compare the tag name of each element in the abstract syntax tree (AST), which is returned by GraphQL. If the element tag matches against the component map (in this case it's book-affiliate-link ) then it will to hydrate it with BookAffiliateLink React component.

import rehypeReact from "rehype-react"
import { MyComponent } from "../pages/my-component"

const renderAst = new rehypeReact({
    createElement: React.createElement,
    components: { "book-affiliate-link": BookAffiliateLink }
}).Compiler

Replace dangerouslySetInnerHTML with the renderAst function we introduced above because we want to render out the custom component in React instead of static HTML.

// <div dangerouslySetInnerHTML={{ __html: post.html }} />
<div>{renderAst(post.htmlAst)}</div>

Update GraphQL query

The final step is to update the GraphQL query, so we get the result in the correct (AST) form.

Previously, I was querying for the Markdown like this.

body {
  childMarkdownRemark {
    html
  }
}

You could replace html with htmlAst, however, since I need html will do a word count I now query both.

body {
  childMarkdownRemark {
    htmlAst
    html
  }
}

Final result

Now when I add the following into my Markdown document.

<book-affiliate-link title="Getting Things Done" subtitle="The Art of Stress-Free Productivity" author="David Allen" link="<https://amzn.to/30WuPNm>" imgsrc="<https://images-na.ssl-images-amazon.com/images/I/41iI4SMqzCL._SX318_BO1,204,203,200_.jpg>"></book-affiliate-link>

The following book component will appear.

Book Affiliate Link Component