React server components + MDX

Hi! 👋 This is a demo of React server components with MDX to show that they can work together. The source code is on GitHub.

What you’re reading right now is <Content />, an MDX file as a component!

💁‍♀️ It’s going to take like at least a year for the stuff discussed and shown here to become stable.
This demo is showing cool stuff coming in the future, don’t depend on this!

What?

Server components can be boiled down to doing the work on a server and being very smart about which components and data are sent to the client. It’s a rather seamless authoring experience. This demo is compiled ahead of time so “server” can also be a build step. MDX is a mix between markdown and JSX. It’s a great way for combining content with components.

For an example, what you’re reading here is MDX as a server component (so it’s compiled on the server), but it includes dynamic client components. Try it out:

You exactly 0 times.

The above <Counter /> is a client component: it’s sent to the client. As this whole MDX document is a server component, it can include more server components. Here is an example of a phyllotaxis as a server component, <Phyllotaxis />, which is static:

Whether an .mdx (or .js) file is a server or client component is defined by their extension (respectively, xxx.server.{js,mdx} or xxx.client.{js,mdx}). You can open your dev tools to see that only the <Counter /> and the embedded <Confetti /> are sent to clients (in 0.index.js). The rest is compiled away.

So how is this different from MDX, currently? Well: an .mdx file is treated like any other component right now. So sure, it can be rendered on the server, but then it can’t include interactive things (such as <Counter />). Or it can be rendered on the client, but then a lot of otherwise static things (<Phyllotaxis /> and all these paragraphs) are rendered there, too. This is solved with RSC.

Why?

React server components run on the server and have zero impact on bundle size. They seamlessly integrate with client components. The hot sauce™ (as in, tooling) that combines them results in a perfect hybrid blend. MDX is nice on top for more content heavy things, because it’s so much nicer to write *emphasis* than <em>emphasis</em> for pages such as this one.

How?

First, react-server-dom-webpack needs to be patched to treat MDX files the way it treats JS files. Hopefully the React team will allow .mdx or make extensions configurable.

The second step makes sure you can require/import .mdx files in Node and depends on whether you’re using CJS or ESM. In CJS (which has require calls, discouraged), add a require('xdm/register.cjs') call somewhere in your server next to where you’re doing require('react-server-dom-webpack/node-register'). In ESM (recommended), import xdm/esm-loader.js, @node-loader/babel, and react-server-dom-webpack/node-loader, then combine them in a node-loader.config.js like so, and finally run your server with --experimental-loader @node-loader/core.

Lastly, make sure webpack can bundle .mdx files. Add something along the lines of {test: /\.mdx$/, use: 'xdm/webpack.cjs'} to module.rules in the webpack config.

A few more things!

💁‍♀️ This section walks through some things I added which are currently complex to solve with RSC. The wiring I’m doing here is supposed to be much easier when RSC ships, and/or handled for you by something like Next.

The React notes demo by the React team sends an empty HTML shell to the client, just like a client-only app. The intended solution (which isn’t built out yet) is to send rendered HTML, which is then hydrated by the client. This demo shows that that can work: this page is rendered on the server and later hydrated, so that users (even those without JavaScript on) can immediately start reading. 🕚 ⬅️ 🚙 💨

The first thing the client in React notes does is ask the server: “hey, what data and components do I need to render this page?” The server responds by streaming the needed data and URLs for the needed components. Can that first network request be removed? So I tried embedded that first response into the HTML to save a roundtrip. One idea on how to do it, is to embed the payload in a <script type=text> … </script>. It’s a pretty good idea but there’s a big catch: you can’t include the characters </script> inside a script. Character references (&lt;) don’t work in them either. So that didn’t work. Instead, this demo embeds the payload at the bottom of the page in hidden PNG image (🤯). Using a PNG compresses a bit worse than a <script> but both are otherwise just as fast. See the bin2png and png2bin repo for more about the method.

This payload to hydrate the page currently contains the whole page. In a perfect world that wouldn’t be necessary because we’ve already rendered it on the server and most of the data isn’t needed because a lot here is static. Sending only what’s needed for faster and smaller hydration, is another thing the React team is planning on solving before RSC ship.

Finally, this demo is a static site, rendered in a build step, so that it can be deployed to any static hosting service (GH pages, Netlify, etc). And this demo is 100% ESM.

When?

Server components are very much in the danger zone. They are not ready for adoption. You shouldn’t use this! Again: it’s going to take at least another year for things to become stable in this space. As for xdm, that’s stable!

Recap

Who?

Hacked together by @wooorm. Thanks to @gaearon for the tips.

Fork me on GitHub.

P.S., here’s that image mentioned before, the data used to hydrate this page: