70 %
Chris Biscardi

Components vs AST manipulation in MDX

MDX presents a new paradigm for people currently using remark (or other markdown rendering approaches). When using remark the most common option to get shortcodes, or other novel behavior, inside a markdown file is to do AST manipulation via remark plugins. This has led to an ecosystem of plugins that assume you're using remark and thus directly insert html blobs into the remark AST. This is an example of plugins assuming that they're going to be run in a specific environment with a specific use case.... but this post isn't really about assumptions people made before there was a different option, not panning out for that new option. That's a fact of life in engineering; Your assumptions today probably won't hold up to the future.

This post is about composition, it's about components, and it's about React.

ASTs are unfamiliar

MDX presents new opportunities to democratize the implementation of interesting functionality both in how we render markdown, and how we construct markdown. Restricting novel behavior to AST manipulation means that people who know how React works, but that didn't take an AST course in college, might be at a loss when trying to progress. That's not to say AST knowledge isn't useful, it's increasingly a power tool in today's JavaScript world because of tools like Babel and jscodeshift.... but it's still a power tool.

Powerful tools that manipulate ASTs often require vast knowledge to apply consistently to generic situations. While this knowledge is both achievable and within reach of many people, we don't always have the time to upskill into a new skill set. The more we provide for the time-strapped "can I get this done in the next 30min before I leave work" engineer, the more people it will be accessible to generally.

React is a great option in this space. Writing React components is a skill that is probably more ubiquitous than AST manipulation is in the JavaScript community, especially amongst beginners. Moving away from AST manipulation as our primary tool for modifying markdown also enables the use of powerful abstractions that are only getting more powerful: Suspense, Hooks, dynamic imports, etc.

A new model

Like I said earlier in this post: MDX allows us to change the way we render markdown and also the way we modify the content of markdown itself. Using the component model that has established itself over the last few years means we can enable more people to build more powerful interfaces while keeping the overhead for content editors to a minimum.

Rendering Markdown

Markdown typically renders to HTML. A small markdown document such as:

A Heading

some content

JS
const thing = {}

can be visualized as HTML like:

html
<h1>A Heading</h1>
<p>some content</p>
<pre>
<code>
const thing = {}
</code>
</pre>

With MDX, we can replace each of these elements with a React component:

JS
<MDXProvider
components={{
h1: props => <h1 {...props} />,
p: props => <p {...props} />,
pre: props => <pre {...props} />,
code: props => <code {...props} />
}}
>
{mdx}
</MDXProvider>

This by itself isn't super powerful yet. We may attach some styles to the heading, paragraph and other elements. What is powerful is the way we can take control of the element to do... literally anything JavaScript can do. We can pull in npm packages like prism-react-renderer or react-live, which handle syntax highlighting and creating a code playground that works inside your browser respectively. We can pass the code string to these npm packages and get interesting functionality almost for free.

We can also start replacing AST manipulation with React components. gatsby-remark-autolink-headers adds a generated id field to all of our headings... and we can do that in a React component.

JS
import slugify from "slugify";
export default ({ children, ...props }) => (
<h1 id={slugify(children)} {...props}>
{children}
</h1>
);

This gives us more control over how the ids are generated too, slugify takes options like lower to lowercase headings or replace to replace spaces with a specific character. You could even use your own or a different npm package that offers this functionality as a function that can be composed with other functions to build in interesting functionality to your components.

JS
export default function getId(props) {
return slugify(props.children);
}

hooks can be used to maintain interesting state. Here's on that simply tracks hover state.

JS
export default props => {
const [isHovered, ref] = useHover();
return (
<h1
id={getId(props)}
ref={ref}
css={{ color: isHovered ? "ref" : "blue" }}
>
{props.children}
</h1>
);
};

We can expand this to even more interesting applications like rendering a markdown code block as a language-specific playgound that calls out to an API to run code and displays the result. Each language can be lazy-loaded on-demand for client-side rendering and rendered however we want on the server. go, Pony, and Babel have playgrounds/repls that could be used liked this.

Markdown Content

So replacing components can get pretty awesome, but we still have the problem of introducing novel new functionality to the content of our markdown. Typically this is done via custom shortcode syntax that abuse the inline code syntax or introduce new meaning to existing syntax like []. Instead of introducing custom syntax for shortcodes, we can rely on an established, common, syntax: JSX.

JSX is powerful enough to do everything we need from out shortcodes: basic declarations, attributes, and children. It even has additional super powers. In the near future, we could extend features like Gatsby's StaticQuery to be automatically handled through React Components and Babel manipulation. We could literally write:

JS
<RelatedPosts tag={["gatsby"]} />

and render a list of related posts to the current blog post that get fetched through Gatsby's GraphQL data layer at build time. The possibilities of replacing custom shortcode implementations with React components is interesting and useful.

Fin

In this post I've tried to convince you that remark and the AST manipulation it requires is a worse fit for the problems we are trying to solve than MDX and React components. That's not to say AST manipulation doesn't have it's uses, but they should be relegated to advanced integration use cases like optimizing the images referenced in your markdown at build time and shipping blur-up features for each of them. The future is components and that will require a mind shift from the world of remark and other markdown processing tools.