React In Markdown (In React)
Update, March 2019: This article is out of date. You can find my article on the new approach, using
This website is made with Gatsby.js, a static site generator for React. You can look at its source here. Gatsby packages up React components, Markdown files, and other assets, creating bundles of code that load as fast as possible. The experience developing a Gatsby site is also the best I’ve ever had, including a hot-reloading development server and a zero-config setup. I’m smitten.
My one complaint with Gatsby was that I wanted to be able to embed React components inside of Markdown. Say you want to make a cool D3 visualization to add to your blog post. Or maybe you want to make an ad-hoc interactive survey form. React (fortunately, in my opinion) forces you to think about your code in components, so it seemed like a significant loss not to be able to use components within a blog post. Surely I wasn’t the only one who felt the same way, so I did a quick search on Gatsby’s Github page, and sure enough, someone had opened up an issue in June 2016 asking:
Is there an easy way to use React components in my Markdown source. Something like reactdown?
I subscribed to get notifications on any further discussion of the topic and went about my way, waiting for someone smarter than myself (or at least more familiar with React internals and compiling HAST) to come along and offer up a solution.
Enter Custom Components
A few more months of waiting, et voila! A week ago, a developer named Ryan Kennedy submitted a PR showing how to embed components in Markdown, using a package called rehype-react.
I won’t rehash how to add custom components to your own Gatsby site, since there are comprehensive instructions here. I’d just like to show a few examples of why being able to do so is so exciting, and how I figured out a few quirks of the approach along the way.
My initial use case for wanting to embed a react component was that I wanted to add a Medium-esqe image zoom effect for my blog. I had found a React package react-medium-image-zoom that seemed like it would work great if only I could use React components. I figured out a hacky solution by conditionally including a different (non-React) library for blog posts that had images, but this felt gross because the library didn’t get rolled up into the bundle served by Gatsby, and was thus unoptimized. After figuring out how to add custom components, I happily swapped that approach out for one using an actual React component.
Here’s what it looks like:
Due to a quirk with the way components in Markdown are parsed, all of the props are passed with lowercase keys and string values. Because
ImageZoom expects props
zoomImage to be objects, it was necessary to write a simple convenience wrapper to pass in the values as it expects them (code here).
Then, you simply include it in Markdown like so:
<zoom-image src='lake-22.jpg' zoomSrc='lake-22-zoom.jpg' caption='Lake 22, WA'></zoom-image> <hidden> <!-- These don't actually get rendered to the DOM --> <img src='lake-22.jpg' /> <img src='lake-22-zoom.jpg' /> </hidden>
<hidden /> component here is a hacky solution to a Webpack issue I haven’t quite figured out yet. Gatsby has a sophisticated asset pipeline which takes relative paths to assets and replaces them with cache-busting paths in the
/public directory (which is what ultimately gets deployed). This means you can maintain an organized asset structure, with each post’s folder containing relevant assets, while not worrying about the actual path of the asset.
<img src='lake-22.jpg' />
gets replaced with something like
<img src='/lake-22-9eef24d18518fa6f6576ccd96c3927a0.jpg' />
The issue is: without including the
<img /> tags, Webpack doesn’t replace the path passed to
ZoomImage, which ultimately ends 404ing, since there isn’t anything at
/blog/react-in-markdown/lake-22.jpg. Including them in the Markdown causes Webpack to replace the path for both that
img as well as the one created by the
ZoomImage component. Interestingly,
<hidden /> is a component which returns
null (you can look at its source here), and therefore doesn’t render its children, so the extra
img tags aren’t actually added to the DOM. Simply including them in the Markdown is enough to make Webpack to do its magic.
Another interesting issue is that the
<hidden /> component can appear directly before or after the
ZoomImage component, but if there’s a blank line of whitespace or more in between, the paths don’t get replaced. I’m going to keep on digging into this issue to try and figure out a way to get around this hacky fix, but for now it works fine and doesn’t ultimately affect page performance.
A few other examples
Custom components allow you to add all sorts of interactive elements to blog posts. What about a timer that shows the amount of time since this page was loaded?
Seconds since component was mounted: 0.
Or maybe a spinning rainbow torus knot?
Why not? (Shh.) The possibilities are endless!
A level deeper
I think it’s a common impulse for developers to push recursion as far as possible. How deep does the rabbit hole go? That being the case, my next question was obvious: would it be possible to write a React component that renders Markdown… inside of Markdown rendered by React?
I’ll cut to the chase: yes, yes it is.
this textarea is editable
Insert Xzibit meme here.
One of the exciting things about working with both React and Gatsby is how much momentum there is behind adding features and improving tooling and performance. There are many different approaches to use components in Markdown and I’m sure there will be lots of continued discussion and development of the topic. Just yesterday I came across a proposal from Zeit’s Guillermo Rauch discussing a
.mdx file format which would marry Markdown and JSX for use cases such as this. There seems to be lots of community support around finding a solution to this common issue, so I’m excited to see what comes next.
Update, a year later
In case you didn’t notice the update up top: this approach using
rehype-react is out of date.
MDX has been released and has over 6,000 stars on GitHub! Furthermore, there’s a package called gatsby-mdx which facilitates using
MDX with Gatsby. I recently updated this blog to use
gatsby-mdx; I wrote about that process here.