6 min read · tagged remark, React, components
What if you want custom UI interactions embedded in your Markdown?
By using rehype-react
with the htmlAst
field, you can write custom React components and then reference them from your Markdown files, or map generic HTML elements like <ul>
or <h2>
to your own components.
Note: this functionality was added in version 1.7.31 of gatsby-transformer-remark
Write the component the way you normally would. For example, here’s a simple “Counter” component:
import React from "react"
const counterStyle = {
/* styles skipped for brevity */
}
export default class Counter extends React.Component {
static defaultProps = {
initialvalue: 0,
}
state = {
value: Number(this.props.initialvalue),
}
handleIncrement = () => {
this.setState(state => {
return {
value: state.value + 1,
}
})
}
handleDecrement = () => {
this.setState(state => {
return {
value: state.value - 1,
}
})
}
render() {
return (
<span style={counterStyle}>
<strong style={{ flex: `1 1` }}>{this.state.value}</strong>
<button onClick={this.handleDecrement}>-1</button>
<button onClick={this.handleIncrement}>+1</button>
</span>
)
}
}
In order to display this component within a Markdown file, you’ll need to add a reference to the component in the template that renders your Markdown content. There are five parts to this:
Install rehype-react
as a dependency
# If you use Yarn
yarn add rehype-react
# If you use npm
npm install --save rehype-react
Import rehype-react
and whichever components you wish to use
import rehypeReact from "rehype-react"
import Counter from "../components/Counter"
Create a render function with references to your custom components
const renderAst = new rehypeReact({
createElement: React.createElement,
components: { "interactive-counter": Counter },
}).Compiler
I prefer to use hyphenated names to make it clear that it’s a custom component.
Render your content using htmlAst
instead of html
This will look different depending on how you were previously referring to the post object retrieved from GraphQL, but in general you’ll want to replace this:
<div dangerouslySetInnerHTML={{ __html: post.html }} />
with this:
{
renderAst(post.htmlAst)
}
Change html
to htmlAst
in your pageQuery
# ...
markdownRemark(fields: { slug: { eq: $slug } }) {
htmlAst # previously `html`
# other fields...
}
# ...
Now, you can directly use your custom component within your Markdown files! For instance, if you include the tag:
<interactive-counter></interactive-counter>
You’ll end up with an interactive component:
0
In addition, you can also pass attributes, which can be used as props in your component:
<interactive-counter initialvalue="10"></interactive-counter>
10
You can also map a generic HTML element to one of your own components. This can be particularly useful if you are using something like styled-components as it allows you to share these primitives between markdown content and the rest of your site. It also means the author of the Markdown doesn’t need to use any custom markup - just standard markdown.
For example if you have a series of header components:
const PrimaryTitle = styled.h1`…`
const SecondaryTitle = styled.h2`…`
const TertiaryTitle = styled.h3`…`
You can map headers defined in markdown to these components:
const renderAst = new rehypeReact({
createElement: React.createElement,
components: {
h1: PrimaryTitle,
h2: SecondaryTitle,
h3: TertiaryTitle,
},
}).Compiler
And headers defined in markdown will be rendered as your components instead of generic HTML elements:
# This will be rendered as a PrimaryTitle component
## This will be rendered as a SecondaryTitle component
### This will be rendered as a TertiaryTitle component
Although it looks like we’re now using React components in our Markdown files, that’s not entirely true: we’re adding custom HTML elements which are then replaced by React components. This means there are a few things to watch out for.
JSX allows us to write self-closing tags, such as <my-component />
. However, the HTML parser would incorrectly interpret this as an opening tag, and be unable to find a closing tag. For this reason, tags written in Markdown files always need to be explicitly closed, even when empty:
<my-component></my-component>
HTML attribute names are not case-sensitive. gatsby-transformer-remark
handles this by lowercasing all attribute names; this means that any props on an exposed component must also be all-lowercase.
The prop in the
Counter
example above was namedinitialvalue
rather thaninitialValue
for exactly this reason; if we tried to accessthis.props.initialValue
we’d have found it to beundefined
.
Any prop that gets its value from an attribute will always receive a string value. Your component is responsible for properly deserializing passed values.
<my-component value=37></my-component>
will still receive the string "37"
as its value instead of the number 37
.true
; for example, if you write <MyComponent isEnabled />
then the props would be { isEnabled: true }
. However, in your Markdown, an attribute without a value will not be interpreted as true
; instead, it will be passed as the empty string ""
. Similarly, passing somevalue=true
and othervalue=false
will result in the string values "true"
and "false"
, respectively.JSON.parse()
in your component to get the value out; just remember to enclose the value in single quotes to ensure it is parsed correctly (e.g. <my-thing objectvalue='{"foo": "bar"}'></my-thing>
).Notice in the
Counter
example how the initialvalue
has been cast using theNumber()
function.
Custom components embedded in Markdown enables many features that weren’t possible before; here are some ideas, starting simple and getting complex:
<countdown-clock> 2019-01-02T05:00:00.000Z </countdown-clock>
<hover-card subject="ostrich"> ostriches </hover-card>
to show a link that lets you hover to get information on ostriches.Jay Gatsby is a mysterious millionaire with shady business connections.