Skip to content
This repository was archived by the owner on Apr 9, 2026. It is now read-only.

Add support for React Server Components#331

Merged
brkalow merged 6 commits into
hashicorp:mainfrom
timneutkens:main
Jan 19, 2023
Merged

Add support for React Server Components#331
brkalow merged 6 commits into
hashicorp:mainfrom
timneutkens:main

Conversation

@timneutkens

@timneutkens timneutkens commented Jan 16, 2023

Copy link
Copy Markdown
Contributor

A small note before the explaining the changes:
This PR is not an endorsement from me for using remote MDX.

Before using MDX as a remote content source it's important to know that the way MDX works is by first compiling the MDX to JavaScript and then executing it. What this means in practice is that MDX is similar to fetching a JavaScript file and then executing it on your server which can lead to remote code execution (RCE) as the MDX file can run any JavaScript.
This is especially important to keep in mind when the source of the MDX content is an external party.

With that explained this is still a commonly used package in the ecosystem which is why I'm opening this PR.


This PR introduces a backwards compatible change that allows the following usage:

// app/page.js (in Next.js)
import { MDXRemote } from "next-mdx-remote/rsc";

export default function Home() {
  return (
    <MDXRemote
      source={`
      # Hello World
      This is from Server Components!
    `}
    />
  );
}

It introduces a separate export under next-mdx-remote/rsc because the exported component is slightly different:

  • It no longer accepts serialized properties, instead it's a async function React Server Component
  • It handles compiling the MDX as part of the component instead of in a separate step
  • It's a React Server Component, can't be used as a Client Component because of the async work.
  • The provided MDX can render Client Components
  • No longer supports lazy as that relied on it being a client component (useState, useEffect)
  • Supports React Suspense out of the box by leveraging the async function React Component, if there's a suspense boundary above or around the MDX content you can show a loading state
  • The new component no longer supports passing components through React Context because that is not supported in Server Components.

Examples

Default usage

// app/page.js (in Next.js)
import { MDXRemote } from "next-mdx-remote/rsc";

export default function Home() {
  return (
    <MDXRemote
      source={`
      # Hello World
      This is from Server Components!
    `}
    />
  );
}

Showing a loading state while the source is processed

// app/page.js (in Next.js)
import { MDXRemote } from "next-mdx-remote/rsc";

export default function Home() {
  return (
    // Ideally this loading spinner would ensure there is no layout shift, 
    // this is an example for how to provide such a loading spinner.
    // In Next.js you can also use `loading.js` for this.
    <Suspense fallback={<>Loading...</>}>
	    <MDXRemote
	      source={`
		  # Hello World
	      This is from Server Components!
	    `}
	    />
    </Suspense>
  );
}

Providing a list of custom components

// components/mdx-remote.js
import { MDXRemote } from "next-mdx-remote/rsc";

const components = {
	h1: (props) => <h1 {...props} className="large-text">{props.children}</h1>
}

export function CustomMDX(props) {
	return <MDXRemote
	  {...props}
      components={{ ...components, ...(props.components || {})  }}
    />
}
// app/page.js (in Next.js)
import { CustomMDX } from "../components/mdx-remote";

export default function Home() {
  return (
    <MDXRemote
      // h1 now renders with `large-text` className
      source={`
      # Hello World
      This is from Server Components!
    `}
    />
  );
}

Frontmatter

The current setup has a two-step process where you first serialize and then pass those props to the React component. With Server Components that is no longer needed and you can use <MDXRemote> with a source property directly. However that means the frontmatter can only be used as part of the MDX source and not to render separate elements.

In order to solve that case there is a separate compileMDX function exported that can be executed in a server component to get both the content and the frontmatter. compileMDX takes exactly the same props as <MDXRemote>.

// app/page.js (in Next.js)
import { compileMDX } from "next-mdx-remote/rsc";

export default async function Home() {
  const {content, frontmatter} = compileMDX({
     source: `
      # Hello World
      This is from Server Components!
    `
    options: { parseFrontmatter: true }
     // components: { h1: (props) => <h1 {...props} className="large-text">{props.children}</h1>  }
  })
  return (
    <>
      <h1>{frontmatter.title}</h1>
      {content}
   </>
  );
}

@timneutkens

Copy link
Copy Markdown
Contributor Author

Added the fixtures needed for the tests but because of the way the package itself is not isolated running it seems to break the app. Seems the test suite should be updated to use a standalone version of the package, you can have a look at the Next.js test suite / copy that behavior, unfortunately I don't have time right now to port all that over.

@brkalow

brkalow commented Jan 16, 2023

Copy link
Copy Markdown
Contributor

Plan for tests so I don't forget. This gets around the error when importing from ./jsx-runtime.cjs:

  1. pretest: npm pack --workspace "next-mdx-remote"
  2. copy test fixture(s) into tmp dir
  3. In copied fixture dir: npm install <path-to-npm-packed-tarball>
  4. next build / next dev
  5. run test cases

I'll work on this tomorrow so we can get a release out with your change here. Thanks!

@timneutkens

timneutkens commented Jan 18, 2023

Copy link
Copy Markdown
Contributor Author

@brkalow potentially helpful this is the util we use for Next.js to create standalone directories for tests: https://github.com/vercel/next.js/blob/canary/test/lib/e2e-utils.ts#L124

@brkalow

brkalow commented Jan 19, 2023

Copy link
Copy Markdown
Contributor

Going to merge this as-is and follow-up with docs and test updates, thanks again! 🙏

@brkalow
brkalow merged commit ae6c486 into hashicorp:main Jan 19, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants