Dynamically imported components in MDX and Next.js
This article relies on choices I've made when I moved my blog from Gatsby to Next, this could be applied differently in other cases. You might want to check the article about it : From Gatsby to Next but still static.
So here I am, back at over-engineering my blog again like 99% of all developers – yes I'm looking at you. I wouldn't advise doing this except if you like banging your head on your desk every now and then. I've moved my blog to Next.js for a while now and was using exclusively static Markdown for articles. There is still an article from 2014 – I was using Wordpress at the time – that I need to get back. It has a gallery with a few embedded pictures embedded. So how can we get that back?
First of all, you need to know there is another way. By making a specific article type within your layouts, listing pictures in the frontmatter and using a different template, life would be easier. But as an engineer, I like abstractions and I will die because of it there has been a nice thing in town recently allowing you to pimp up your blog's articles. That is MDX.
So what is MDX?
MDX is a syntax enabling the use of JSX within Markdown. You can then import components and make your blog articles interactive. For example, you might want charts or surveys and while you can embed some HTML in your static Markdown documents, you can't really get all the power that Javascript would give to you.
Using MDX with Next
Integrating MDX into Next is pretty easy, just use the official @next/mdx
and @mdx-js/loader
packages and add this to your next.config.js
:
Just put .mdx
files in your pages, or import them into your pages just like a regular React component, it works!
Now, you might want features like frontmatter – not supported by default in MDX 1, and 2 is not fully released yet. You can make your own webpack config for that:
This enables MDX files to be imported and treated as components alongside a frontMatter
export with all the file's data you specified. Pretty easy, there is even an official blog post about it.
If all your documents lies within the pages
structure, you'll get all you need. Just going through the docs would get you a way to personalize the layout and you'd be fine. You can go rest now, forget about it and enjoy your day.
But.
If you want to dynamically import your files, you'll notice all you get is a ready-to-be-rendered component.
Problem: Next.js needs serializable content in getStaticProps
.
With static Markdown, we used to get the raw content and give it as-is to Next.js, rendering it within the component. How could we do the same with a component-like document. We need serialization.
1st workaround: loading content as we render
As we can't serialize our content, we will remove this property and instead loading it dynamically when the page is rendered. Everything dynamic about your components in your MDX will get mounted afterwards, but you will get a static version first. That's it, no?
When dynamically loading a path, webpack bundles everything it finds that could match the path. You could use things like magic comments to narrow down the files imported but you'd still get every single article reference bundled within the page. What we want is just having a pre-rendered static version of the article and a reference to the dynamic version of it.
2nd workaround : using next-mdx-remote
By the time, I started exploring a solution for this (half a year ago), I stumbled upon next-mdx-enhanced
and next-mdx-remote
that target exactly what I enunciated before. The latter gets the content, render a static version and hydrate it back during render.
Here, we can keep our frontmatter loader, but we need to drop anything else. We get the source and give it to renderToString
. It does a little bit of magic and gives you back a serialized version you can render both server-side and client-side. You can also provide global components that gets rendered with every document (it works with next/dynamic
too!). If you want to explore this solution, there's an official example within Next.js's repo.
Recently, an interesting PR got submitted: Refactor hydrate and renderToString. It greatly simplifies the internals and provides a great component interface instead of the hydrate function above. It's not ground-breaking but it's always nice to get a simplied API.
But there's still a downside with this library though. Imports are not supported within the MDX document.
The thing is, when you look at the code, nothing magic is happening really. The document is just processed through the MDX compiler and babel. I've researched and explored some solutions with plugins for those but nothing came with success.
3rd and last workaround : let's make a webpack bundle instead
This solution, even though very complex, is what appeared to me as being the only way to make everything works as I wanted. When you think of it, webpack already does a great job loading serialized bundle chunks and executing them at the right time.
Why couldn't we hydrate a webpack JS bundle the same way next-mdx-remote
does it for a raw transformed script? That's how I got into the troubles of rendering a webpack bundle within Next's webpack process. Maybe you could do this in one go with plugins, but my lack of experience with them and webpack's core makes it difficult for me.
So what does the process looks like when dynamically importing an article?
The code here is a bit simplified, the full version involves the use of a memory filesystem to avoid writing on the disk. I'm not sure how yet, but I think it'd be better if we could process the content coming into the loader instead. There might be a need to check how to make the virtual and the real filesystem work together for imports.
- It goes through a custom loader handling what we locally require (local components, images...)
- It compiles a raw bundle for it
- It gets back in Next.js's
getStaticProps
and returned as a prop. - It's hydrated the same way as
next-mdx-remote
's new PR.
Frankly, this is very experimental but I don't see how we could not get this as a behavior built in Next.js directly. For now, what we require locally is pretty much isolated from the rest of your app, so be careful when importing libraries.
With my limited experience with webpack, the next steps could be reducing the article's bundle size, minification and optimization and also sharing some webpack config with Next's.
Demo time!
Finally, it works! You can try disabling Javascript to see what's displaying here in lieu of the interactive component:
The job is done
Sure, everything works but it raises some leads to future improvements and built-in solutions. Next.js already does a lot with its own webpack magic and with some kind of special API, it could paves the way to more and more dynamic parts being prepared during the static generation and rendered at will. What matters is a way to specify which page will require which dynamic component and we could get something optimized per each page.
You might want to subscribe to this issue to stay informed if anything progresses in that direction: Import MDX components dynamically.