Craig Glennie

Getting imports to work with next-mdx-remote

Prelude

Recently I’ve migrated this site from the deprecated(?) next-mdx-enhanced to it’s replacement next-mdx-remote. The reason I’d been using next-mdx-enhanced is that I wanted frontmatter (without having to set it up myself) and I got that out of the box. The only problem I’d really had with the library was with content not regenerating on my index page after I made changes to an MDX file. I think this was something to do with the MDX files not being watched, and Next not realising there was anything to do. So I would sometimes have to force a refresh by either deleting the local Next cache (annoying) or making any edit to one of the .tsx files - those were being watched. So I had a special //edit this comment to trigger a refresh line in index.tsx. Not really what you want to have to do!

Since next-mdx-remote seems like the future (they’re both by Hashicorp, and next-mdx-enhanced now suggests that you switch) I figured I’d have a go at updating it.

It took a bit of adaptation - the docs are a little sparse (or maybe just a bit too concise) but ultimately did contain enough example code for me to get the MDX files to load. But I ran into a problem: next-mdx-remote doesn’t support import statements in MDX files…

Umm, what?

I’d forgive you for thinking the whole point of using MDX is to have your React components in your Markdown. Otherwise it’s just, you know, Markdown - we have that already.

Their rationale makes sense

in order to work, imports must be relative to a file path, and this library allows content to be loaded from anywhere, rather than only loading local content from a set file path

But, like, I need have the imports or my TVP Ratio Calculator won’t work. Fortunately, there is a way: because you’re already going to be calling the library’s hydrate and renderToString functions, you can import your components and pass them to the function, and they’ll be available to the MDX file.

import ComponentForMDX from "./component";
const components = { ComponentForMDX };
// ...
const content = hydrate(source, { components });
// ...
const mdxSource = await renderToString(content, { components });

Ok, but how do I get that to work when I have a single [slug].tsx page for all my blog posts, and it doesn’t know which posts need which components until the post loads?

Getting the damn imports to work

Well the answer is that you just have to import all the components you might possibly have to pass to one of your MDX files. Honestly this feels so janky that the only way I can see it working on a large site is where the MDX files need a common set of components. If your files each have their own components (like mine do, since each one is a blog post and the components go along with the post) then you’re going to end up with an ugly mess of imports that you have to keep track of.

Here’s my solution. It’s not perfect, but this is my personal site so I’m firmly on the side of “good enough”.

When an MDX file needs to import some components I put the names of those components in an array in the frontmatter:

---
imports: [Calculator]
---

In my [slug].tsx file I import all the components at the top level, collect them in an object, then use the imports values from the frontmatter to decide which ones need to be passed to any given page. Something like this:

import Calculator from "../../content/2020-08-25/calculator";

const allComponents = {
  Calculator,
};

const getComponents = (imports) => {
  const components = {};
  if (imports) {
    imports.forEach(
      (componentName) =>
        (components[componentName] = allComponents[componentName])
    );
  }
  return components;
};

export default function BlogPost({ mdxSource, frontMatter }) {
  const components = getComponents(frontMatter.imports);
  const content = hydrate(mdxSource, { components });
  return <BlogPage title={frontMatter.title}>{content}</BlogPage>;
}

I don’t love it, but it works fine. I don’t really like that I had to figure this out myself (There was a whole lot of trial and error) when it feels like essential functionality, but I got there.