Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/using-remark/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"lodash-id": "^0.14.0",
"lodash-webpack-plugin": "^0.11.2",
"react-typography": "^0.15.10",
"rehype-react": "^3.0.2",
"slash": "^1.0.0",
"typeface-space-mono": "^0.0.22",
"typeface-spectral": "^0.0.29",
Expand Down
47 changes: 47 additions & 0 deletions examples/using-remark/src/components/Counter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from "react"

const counterStyle = {
display: `flex`,
flexFlow: `row nowrap`,
alignItems: `center`,
padding: `0.2em 0.4em`,
borderRadius: `2px`,
backgroundColor: `rgba(0, 0, 0, 0.2)`,
boxShadow: `inset 2px 1.5px 4px rgba(0, 0, 0, 0.2)`,
}

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>
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
---
title: "Custom components"
date: "2018-01-27"
draft: false
author: Jay Gatsby
tags:
- 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.

## Writing a component

Write the component the way you normally would. For example, here's a simple "Counter" component:

```js
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>
)
}
}

```

## Enabling the component

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:

1. Install `rehype-react` as a dependency

```bash
# If you use Yarn
yarn add rehype-react

# If you use npm
npm install --save rehype-react
```

2. Import `rehype-react` and whichever components you wish to use

```js
import rehypeReact from "rehype-react"
import Counter from "../components/Counter"
```

3. Create a render function with references to your custom components

```js
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.

4. 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:

```js
<div dangerouslySetInnerHTML={{ __html: post.html }} />
```

with this:

```js
{renderAst(post.htmlAst)}
```

5. Change `html` to `htmlAst` in your `pageQuery`

```graphql
# ...
markdownRemark(fields: { slug: { eq: $slug } }) {
htmlAst # previously `html`

# other fields...
}
# ...
```

## Using the component

Now, you can directly use your custom component within your Markdown files! For instance, if you include the tag:

```html
<interactive-counter></interactive-counter>
```

You'll end up with an interactive component:

<interactive-counter></interactive-counter>

In addition, you can also pass attributes, which can be used as props in your component:

```html
<interactive-counter initialvalue="10"></interactive-counter>
```

<interactive-counter initialvalue="10"></interactive-counter>

## Caveats

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.

### Always add closing tags

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:

```html
<my-component></my-component>
```

### Attribute names are always lowercased

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 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`.

### Attributes are always strings

Any prop that gets its value from an attribute will always receive a string value. Your component is responsible for properly deserializing passed values.

- 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`.
- 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.
- 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>`).

> Notice in the `Counter` example how the initial `value` has been cast using the `Number()` function.

## Possibilities

Custom components embedded in Markdown enables many features that weren't possible before; here are some ideas, starting simple and getting complex:

- 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>`
- 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.
- 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.
- 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.
- Write a component that wraps a [GFM table](./hello-world-kitchen-sink/#tables) and displays the data from the table in an interactive graph.
11 changes: 9 additions & 2 deletions examples/using-remark/src/templates/template-blog-post.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import React from "react"
import Link from "gatsby-link"
import Img from "gatsby-image"
import rehypeReact from "rehype-react"

import styles from "../styles"
import { rhythm, scale } from "../utils/typography"
import presets from "../utils/presets"
import Counter from "../components/Counter"

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

const renderAst = new rehypeReact({
createElement: React.createElement,
components: { "interactive-counter": Counter },
}).Compiler

class BlogPostRoute extends React.Component {
render() {
const post = this.props.data.markdownRemark
Expand Down Expand Up @@ -69,7 +76,7 @@ class BlogPostRoute extends React.Component {
className="toc"
/>

<div dangerouslySetInnerHTML={{ __html: post.html }} className="post" />
{renderAst(post.htmlAst)}
<hr
css={{
marginBottom: rhythm(1),
Expand Down Expand Up @@ -122,7 +129,7 @@ export default BlogPostRoute
export const pageQuery = graphql`
query BlogPostBySlug($slug: String!) {
markdownRemark(fields: { slug: { eq: $slug } }) {
html
htmlAst
timeToRead
tableOfContents
fields {
Expand Down