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:
- Gatsby needs to understand the React component when parsing the Markdown document. I used
gatsby-remark-component-parent2div
to detect custom components. - 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.