Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for React Server Components #331

Merged
merged 6 commits into from
Jan 19, 2023
Merged

Conversation

timneutkens
Copy link
Contributor

@timneutkens timneutkens commented Jan 16, 2023

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
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
Copy link
Contributor

BRKalow commented Jan 16, 2023

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
Copy link
Contributor Author

timneutkens commented Jan 18, 2023

@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
Copy link
Contributor

BRKalow commented Jan 19, 2023

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 join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants