Skip to content

Commit 9c2dafb

Browse files
ryaninventsKyleAMathews
authored andcommitted
[examples/using-remark]: Custom React components in Markdown files (#3732)
1 parent d7dae32 commit 9c2dafb

File tree

4 files changed

+238
-2
lines changed

4 files changed

+238
-2
lines changed

examples/using-remark/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"lodash-id": "^0.14.0",
3030
"lodash-webpack-plugin": "^0.11.2",
3131
"react-typography": "^0.15.10",
32+
"rehype-react": "^3.0.2",
3233
"slash": "^1.0.0",
3334
"typeface-space-mono": "^0.0.22",
3435
"typeface-spectral": "^0.0.29",
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import React from "react"
2+
3+
const counterStyle = {
4+
display: `flex`,
5+
flexFlow: `row nowrap`,
6+
alignItems: `center`,
7+
padding: `0.2em 0.4em`,
8+
borderRadius: `2px`,
9+
backgroundColor: `rgba(0, 0, 0, 0.2)`,
10+
boxShadow: `inset 2px 1.5px 4px rgba(0, 0, 0, 0.2)`,
11+
}
12+
13+
export default class Counter extends React.Component {
14+
static defaultProps = {
15+
initialvalue: 0,
16+
}
17+
18+
state = {
19+
value: Number(this.props.initialvalue),
20+
}
21+
22+
handleIncrement = () => {
23+
this.setState(state => {
24+
return {
25+
value: state.value + 1,
26+
}
27+
})
28+
}
29+
30+
handleDecrement = () => {
31+
this.setState(state => {
32+
return {
33+
value: state.value - 1,
34+
}
35+
})
36+
}
37+
38+
render() {
39+
return (
40+
<span style={counterStyle}>
41+
<strong style={{ flex: `1 1` }}>{this.state.value}</strong>
42+
<button onClick={this.handleDecrement}>-1</button>
43+
<button onClick={this.handleIncrement}>+1</button>
44+
</span>
45+
)
46+
}
47+
}
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
---
2+
title: "Custom components"
3+
date: "2018-01-27"
4+
draft: false
5+
author: Jay Gatsby
6+
tags:
7+
- remark
8+
- React
9+
- components
10+
---
11+
12+
What if you want custom UI interactions embedded in your Markdown?
13+
14+
By using `rehype-react` with the `htmlAst` field, you can write custom React components and then reference them from your Markdown files.
15+
16+
## Writing a component
17+
18+
Write the component the way you normally would. For example, here's a simple "Counter" component:
19+
20+
```js
21+
import React from "react"
22+
23+
const counterStyle = {
24+
/* styles skipped for brevity */
25+
}
26+
27+
export default class Counter extends React.Component {
28+
static defaultProps = {
29+
initialvalue: 0,
30+
}
31+
32+
state = {
33+
value: Number(this.props.initialvalue),
34+
}
35+
36+
handleIncrement = () => {
37+
this.setState(state => {
38+
return {
39+
value: state.value + 1,
40+
}
41+
})
42+
}
43+
44+
handleDecrement = () => {
45+
this.setState(state => {
46+
return {
47+
value: state.value - 1,
48+
}
49+
})
50+
}
51+
52+
render() {
53+
return (
54+
<span style={counterStyle}>
55+
<strong style={{ flex: `1 1` }}>
56+
{this.state.value}
57+
</strong>
58+
<button onClick={this.handleDecrement}>-1</button>
59+
<button onClick={this.handleIncrement}>+1</button>
60+
</span>
61+
)
62+
}
63+
}
64+
65+
```
66+
67+
## Enabling the component
68+
69+
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:
70+
71+
1. Install `rehype-react` as a dependency
72+
73+
```bash
74+
# If you use Yarn
75+
yarn add rehype-react
76+
77+
# If you use npm
78+
npm install --save rehype-react
79+
```
80+
81+
2. Import `rehype-react` and whichever components you wish to use
82+
83+
```js
84+
import rehypeReact from "rehype-react"
85+
import Counter from "../components/Counter"
86+
```
87+
88+
3. Create a render function with references to your custom components
89+
90+
```js
91+
const renderAst = new rehypeReact({
92+
createElement: React.createElement,
93+
components: { "interactive-counter": Counter },
94+
}).Compiler
95+
```
96+
97+
I prefer to use hyphenated names to make it clear that it's a custom component.
98+
99+
4. Render your content using `htmlAst` instead of `html`
100+
101+
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:
102+
103+
```js
104+
<div dangerouslySetInnerHTML={{ __html: post.html }} />
105+
```
106+
107+
with this:
108+
109+
```js
110+
{renderAst(post.htmlAst)}
111+
```
112+
113+
5. Change `html` to `htmlAst` in your `pageQuery`
114+
115+
```graphql
116+
# ...
117+
markdownRemark(fields: { slug: { eq: $slug } }) {
118+
htmlAst # previously `html`
119+
120+
# other fields...
121+
}
122+
# ...
123+
```
124+
125+
## Using the component
126+
127+
Now, you can directly use your custom component within your Markdown files! For instance, if you include the tag:
128+
129+
```html
130+
<interactive-counter></interactive-counter>
131+
```
132+
133+
You'll end up with an interactive component:
134+
135+
<interactive-counter></interactive-counter>
136+
137+
In addition, you can also pass attributes, which can be used as props in your component:
138+
139+
```html
140+
<interactive-counter initialvalue="10"></interactive-counter>
141+
```
142+
143+
<interactive-counter initialvalue="10"></interactive-counter>
144+
145+
## Caveats
146+
147+
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.
148+
149+
### Always add closing tags
150+
151+
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:
152+
153+
```html
154+
<my-component></my-component>
155+
```
156+
157+
### Attribute names are always lowercased
158+
159+
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.
160+
161+
> The prop in the `Counter` example above was named `initialvalue` rather than `initialValue` for exactly this reason; if we tried to access `this.props.initialValue` we'd have found it to be `undefined`.
162+
163+
### Attributes are always strings
164+
165+
Any prop that gets its value from an attribute will always receive a string value. Your component is responsible for properly deserializing passed values.
166+
167+
- Numbers are always passed as strings, even if the attribute is written without quotes: `<my-component value=37></my-component>` will still receive the string `"37"` as its value instead of the number `37`.
168+
- React lets you pass a prop without a value, and will interpret it to mean `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.
169+
- You can pass object values if you use `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>`).
170+
171+
> Notice in the `Counter` example how the initial `value` has been cast using the `Number()` function.
172+
173+
## Possibilities
174+
175+
Custom components embedded in Markdown enables many features that weren't possible before; here are some ideas, starting simple and getting complex:
176+
177+
- Write a live countdown clock for an event such as Christmas, the Super Bowl, or someone's birthday. Suggested markup: `<countdown-clock> 2019-01-02T05:00:00.000Z </countdown-clock>`
178+
- Write a component that displays as a link with an informative hovercard. For example, you might want to write `<hover-card subject="ostrich"> ostriches </hover-card>` to show a link that lets you hover to get information on ostriches.
179+
- If your Gatsby site is for vacation photos, you might write a component that allows you to show a carousel of pictures, and perhaps a map that shows where each photo was taken.
180+
- Write a component that lets you add live code demos in your Markdown, using [component-playground](https://formidable.com/open-source/component-playground/) or something similar.
181+
- Write a component that wraps a [GFM table](./hello-world-kitchen-sink/#tables) and displays the data from the table in an interactive graph.

examples/using-remark/src/templates/template-blog-post.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import React from "react"
22
import Link from "gatsby-link"
33
import Img from "gatsby-image"
4+
import rehypeReact from "rehype-react"
45

56
import styles from "../styles"
67
import { rhythm, scale } from "../utils/typography"
78
import presets from "../utils/presets"
9+
import Counter from "../components/Counter"
810

911
import "katex/dist/katex.min.css"
1012

13+
const renderAst = new rehypeReact({
14+
createElement: React.createElement,
15+
components: { "interactive-counter": Counter },
16+
}).Compiler
17+
1118
class BlogPostRoute extends React.Component {
1219
render() {
1320
const post = this.props.data.markdownRemark
@@ -69,7 +76,7 @@ class BlogPostRoute extends React.Component {
6976
className="toc"
7077
/>
7178

72-
<div dangerouslySetInnerHTML={{ __html: post.html }} className="post" />
79+
{renderAst(post.htmlAst)}
7380
<hr
7481
css={{
7582
marginBottom: rhythm(1),
@@ -122,7 +129,7 @@ export default BlogPostRoute
122129
export const pageQuery = graphql`
123130
query BlogPostBySlug($slug: String!) {
124131
markdownRemark(fields: { slug: { eq: $slug } }) {
125-
html
132+
htmlAst
126133
timeToRead
127134
tableOfContents
128135
fields {

0 commit comments

Comments
 (0)