Sabigara

How to embed live code editor for React components in MDX docs

Overview

I'd like to share how to embed live code editor for React components in your docs page like Chakra UI's docs. Live means that you can, for instance, click the rendered button and the code is editable: if you change the inner text of the button, the preview reflects the change.

By taking the advantage of the extensibility of MDX, we can (kind of) easily accomplish the functionality which looks hard to implement.

Notation is like this:

```tsx live
<>
  <Button variant="solid">Solid</Button>
  <Button variant="subtle">Solid</Button>
  <Button variant="ghost">Solid</Button>
</>
```

And the rendered page is:

- YouTubeEnjoy the videos and music you love, upload original content, and share it all with friends, family, and the world on YouTube.ファビコンwww.youtube.com

We want a live preview (and a code editor) to be shown if there is a live argument, while a normal code block should be shown if no argument passed.

WARNING

In this article, I assume you already have a statically-generated site powered by a unified pipeline. Take a look at Gatsby or Next.js starters if not.

Add remark-mdx-code-meta plugin

Let's get started from customizing unified pipeline to support the live argument after the triple backtick (code fence) and the name of language. Install remark-mdx-code-meta to do so.

GitHub - remcohaszing/remark-mdx-code-meta: A remark MDX plugin for using markdown code block metadataA remark MDX plugin for using markdown code block metadata - remcohaszing/remark-mdx-code-metaファビコンgithub.com

pnpm add remark-mdx-code-meta
/** @type {import('@mdx-js/mdx').CompileOptions} */
const mdxOptions = {
  remarkPlugins: [remarkMdxCodeMeta, ...otherPlugins],
  rehypePlugins: [...otherPlugins],
};

This plugin enables the support for ```language key=value notation. The key-value pair is passed to the custom Pre component we'll make in the next section.

Custom Pre component

MDX allows us to specify custom components dedicated to each HTML tag by passing an object like { image: Image, pre: Pre }. A code fence is transformed into <pre><code>Your code here</code></pre> so we want to pass a custom component to add the live-preview feature.

The following component examines the content of a pre tag to decide whether it's a code block because pre can be used for other purposes. The role for rendering a preview and highlighted code should be delegated to CodeBlock which is implemented next.

Pre.tsx
import React from "react";
// We'll create this later
import CodeBlock from "@/components/MdxComponents/CodeBlock";

type Props = {
  live?: boolean;
  children?: React.ReactNode;
};

export default function Pre({ live, children, ...props }: Props) {
  if (React.isValidElement(children) && children.type === "code") {
    return (
      <div {...props}>
        <CodeBlock live={live} {...children.props} />
      </div>
    );
  }
  return <pre {...props}>{children}</pre>;
}
MdxComponents.tsx
export const mdxComponents: MDXComponents = {
  pre: Pre,
} as const;

CodeBlock component

We depend on react-live for preview and editor.

GitHub - FormidableLabs/react-live: A flexible playground for live editing React componentsA flexible playground for live editing React components - FormidableLabs/react-liveファビコンgithub.com

CodeBlock.tsx
import React from "react";
import { LiveProvider, LiveError, LivePreview, LiveEditor } from "react-live";
import { type Language } from "prism-react-renderer";
import theme from "prism-react-renderer/themes/vsDark";
import Button from "@/components/Button";

export default function CodeBlock({ children, className, live }: Props) {
  const language = className.replace(/language-/, "") as Language;
  const code = children.replace(/\n$/, "");
  const codeBlock = <code>Render Non-interactive code here...</code>;

  if (live) {
    return (
      <LiveProvider code={code} scope={{ Button }}>
        <LivePreview />
        <LiveError />
        <LiveEditor code={code} language={language} theme={theme} />
      </LiveProvider>
    );
  }

  return codeBlock;
}

There is nothing difficult here thanks to the simple API, but some things to consider:

Summary

The amount of code is not much but it took me a while for finding the correct way to achieve this feature. I like the docs of MUI and Mantine don't cause any layout shift but their implementation is more complex so I give up for now.