next-mdx-remote: Does not yet support Next.js Version 13

I’ve noticed that next-mdx-remote doesn’t work with Next.js 13.

To reproduce the issue, I tried running the official example with-mdx-remote using Next.js 13 and it doesn’t work. Trying to load the example post, I get the following error:

CleanShot 2022-10-26 at 18 41 15@2x

How to reproduce

In my own project, I’m getting a similar error relating to imports of components, so I assume the issue might be related to changes in Next.js’ bundler or build system.

I don’t get the error if I run the tests in this repo (hashicorp/next-mdx-remote) with Next.js 13. For me that’s another hint that the error might be related to bundling, since in this repo the test doesn’t import next-mdx-remote through node_modules (see https://github.com/hashicorp/next-mdx-remote/blob/f5b0e74529908efd78b981bae7121847ed751b58/__tests__/fixtures/basic/pages/index.jsx#L5-L6)

Compiled Output

Here is the compiled output of node_modules/next-mdx-remote/index.js:

import React, { useState, useEffect, useMemo } from 'react';
import { jsxRuntime } from './jsx-runtime.cjs';
import * as mdx from '@mdx-js/react';

if (typeof window !== 'undefined') {
  window.requestIdleCallback =
    window.requestIdleCallback ||
    function (cb) {
      var start = Date.now();
      return setTimeout(function () {
        cb({
          didTimeout: false,
          timeRemaining: function () {
            return Math.max(0, 50 - (Date.now() - start))
          },
        });
      }, 1)
    };

  window.cancelIdleCallback =
    window.cancelIdleCallback ||
    function (id) {
      clearTimeout(id);
    };
}

/**
 * Renders compiled source from next-mdx-remote/serialize.
 */
function MDXRemote({ compiledSource, frontmatter, scope, components = {}, lazy, }) {
    const [isReadyToRender, setIsReadyToRender] = useState(!lazy || typeof window === 'undefined');
    // if we're on the client side and `lazy` is set to true, we hydrate the
    // mdx content inside requestIdleCallback, allowing the page to get to
    // interactive quicker, but the mdx content to hydrate slower.
    useEffect(() => {
        if (lazy) {
            const handle = window.requestIdleCallback(() => {
                setIsReadyToRender(true);
            });
            return () => window.cancelIdleCallback(handle);
        }
    }, []);
    const Content = useMemo(() => {
        // if we're ready to render, we can assemble the component tree and let React do its thing
        // first we set up the scope which has to include the mdx custom
        // create element function as well as any components we're using
        const fullScope = Object.assign({ opts: { ...mdx, ...jsxRuntime } }, { frontmatter }, scope);
        const keys = Object.keys(fullScope);
        const values = Object.values(fullScope);
        // now we eval the source code using a function constructor
        // in order for this to work we need to have React, the mdx createElement,
        // and all our components in scope for the function, which is the case here
        // we pass the names (via keys) in as the function's args, and execute the
        // function with the actual values.
        const hydrateFn = Reflect.construct(Function, keys.concat(`${compiledSource}`));
        return hydrateFn.apply(hydrateFn, values).default;
    }, [scope, compiledSource]);
    if (!isReadyToRender) {
        // If we're not ready to render, return an empty div to preserve SSR'd markup
        return (React.createElement("div", { dangerouslySetInnerHTML: { __html: '' }, suppressHydrationWarning: true }));
    }
    // wrapping the content with MDXProvider will allow us to customize the standard
    // markdown components (such as "h1" or "a") with the "components" object
    const content = (React.createElement(mdx.MDXProvider, { components: components },
        React.createElement(Content, null)));
    // If lazy = true, we need to render a wrapping div to preserve the same markup structure that was SSR'd
    return lazy ? React.createElement("div", null, content) : content;
}

export { MDXRemote };

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 49 (7 by maintainers)

Commits related to this issue

Most upvoted comments

There is a better temporary solution without the need to downgrade @mdx-js/* to 2.1.5. Just add development: false to the mdxOptions argument within the serialize() call:

 const mdxSource = await serialize(content, {
   mdxOptions: {
     remarkPlugins: [],
     rehypePlugins: [],
+    development: false,
   },
});

In mdx-js/mdx#2045, compiled MDX source will use jsxDEV for rendering in development mode, while in production mode it uses jsx and jsxs. jsxDEV should be imported from react/jsx-dev-runtime, but it is not exported by src/jsx-runtime.cjs. This causes the TypeError: _jsxDEV is not a function error.

I am not able to use MDXContent in a clientcomp. I am getting TypeError: _jsxDEV is not a function as an error.

impl code:

// Article.tsx
'use client';

import { MDXRemote, MDXRemoteProps } from 'next-mdx-remote';

export function Article({ content }: { content: MDXRemoteProps }) {
  return <MDXRemote {...content} />;
}
// page.tsx
import { serialize } from 'next-mdx-remote/serialize';
import { Article } from '../../../components/Article';
import { getPostBySlug } from '../../../utils/markdownHelper';
type PageProps = {
  params: { post: string };
  searchParams?: { [key: string]: string | string[] | undefined };
};
export default async function Page({ params }: PageProps) {
  const { content } = getPostBySlug(params.post);
  const mdx = await serialize(content);
  return (
    <div>
      <Article content={mdx} />
    </div>
  );
}

using node 18 w/ next@13.0.6 and next-mdx-remote@^4.2.0

Hey y’all, we’ve now get experimental support for server components, and the app directory. Take a look at the release notes & documentation and give it a try!

Thanks for the report. We haven’t had the opportunity to test it out with v13 yet, but we’ll take a look at this soon!

For anyone else who is blocked by this, you can create a wrapper component (outside of the /app folder) like so:

"use client";

import { MDXRemote, MDXRemoteProps } from "next-mdx-remote";

interface MDXContentProps {
  source: MDXRemoteProps;
}

export default function MDXContent({ source }: MDXContentProps) {
  return <MDXRemote {...source} />;
}

And then within the /app folder, in a page.tsx file:

// This is the wrapper component above.
import MDXContent from "components/MDXContent";
import { serialize } from "next-mdx-remote/serialize";
...
const source = await serialize(article.content);
...
<MDXContent source={source} />

The “use client” directive (see docs) makes this a client component so that it has access to local state. I haven’t done any extensive testing here, but I believe the tradeoff is that next-mdx-remote will be included in the JS bundle sent to the client and that the work of translating will take place there, even if the app is otherwise statically rendered.

I’m just going to piggyback off of this issue rather than creating a new one (can if the author prefers).

There seems to be a separate issue when server/static rendering with Next 13.


export default async function Post(props: Props) {
  const source = await serialize("Some **mdx**");
  return (
    <div>
      <MDXRemote {...source} />
    </div>
  );
}

will result in the following error

Warning: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
TypeError: Cannot read properties of null (reading 'useState')

However, wrapping the component to force it to client render like so

"use client";

import { MDXRemote, MDXRemoteProps } from "next-mdx-remote";

export default function MDXRemoteWrapper(props: MDXRemoteProps) {
  return <MDXRemote {...props} />;
}

It seems to “solve” the issue.

I’m too tired to look into it anymore tonight but may poke around tomorrow if no one else is looking into it. But it seems like wrapping it like this is needed without some reorganization of the project, so the MDXRemote component gets loaded in with the “use client” directive.

Just a small note here - we are aware on the nextjs team that the app directory doesn’t yet support mdx-remote. We have it on our roadmap to address this, but aren’t quite there yet. We’re still working on fixing critical bugs and stabilizing it right now, as it’s still beta.

I’ll drop updates here when we start working on this!

Can confirm this works.

For the npm users, the key here is “overrides”

 "overrides": {
    "@mdx-js/mdx": "2.1.5",
    "@mdx-js/react": "2.1.5"
  }

I am not able to use MDXContent in a clientcomp. I am getting TypeError: _jsxDEV is not a function as an error.

It works if you force-downgrade @mdx-js to 2.1.5:

"resolutions": {
	"@mdx-js/mdx": "2.1.5",
	"@mdx-js/react": "2.1.5"
},

I opened a PR (#323) to fix this issue. Before it gets merged, you can use the temporary workaround.

Hi y’all,

As discussed above, we should fully support next 13 as well as the /app directory, documented here). I’m going to close this out, if you have specific issues working with next-mdx-remote and next 13 going forward, please feel free to open additional issues with reproductions. Thanks for the discussion here!

I ran into the same issue and solved it using {/* @ts-expect-error Server Component */}.

It’s documented on the Next 13 beta docs.

To give a current summary. It took me a while, and I was getting the error back after updates etc. This definitly works:

...
"next": "^13.0.7",
"next-mdx-remote": "^4.2.1",
...

Do not use this 4.2.0 workaround anymore. Otherwise, you get the error back. Credits to @evowizz for the hint.

I can say that I still get the same error

After updating to the version 4.2.1, make sure to revert the temporary solution shared by @imtsuki. Essentially, you’d end up doing this:

 const mdxSource = await serialize(content, {
   mdxOptions: {
     remarkPlugins: [],
     rehypePlugins: [],
-    development: false,
   },
});

I don’t know if this is related but after upgrading to Next 13.1 we started getting this error:

TypeError: _jsx is not a function
image

Note: We’re not using the app directory anywhere yet.

EDIT:

Here's what the compiled source looks like
{
  source: {
    compiledSource: '/*@jsxRuntime automatic @jsxImportSource react*/\n' +
      'const {Fragment: _Fragment, jsx: _jsx, jsxs: _jsxs} = arguments[0];\n' +
      'const {useMDXComponents: _provideComponents} = arguments[0];\n' +
      'function _createMdxContent(props) {\n' +
      '  const _components = Object.assign({\n' +
      '    p: "p",\n' +
      '    h4: "h4"\n' +
      '  }, _provideComponents(), props.components);\n' +
      '  return _jsxs(_Fragment, {\n' +
      '    children: [_jsx(_components.p, {\n' +
      '      children: "Vital App is an app that can can help you combine your health peripherals with your calendar."\n' +
      '    }), "\\n", _jsx(_components.h4, {\n' +
      '      children: "Supported Actions:"\n' +
      '    }), "\\n", _jsx(_components.p, {\n' +
      '      children: "Sleep reschedule automation: Had a hard night? 🌕\\nAutomatically reschedule your whole day schedule based on your sleep parameters. (Setup your desired configuration on installed apps page.)"\n' +
      '    })]\n' +
      '  });\n' +
      '}\n' +
      'function MDXContent(props = {}) {\n' +
      '  const {wrapper: MDXLayout} = Object.assign({}, _provideComponents(), props.components);\n' +
      '  return MDXLayout ? _jsx(MDXLayout, Object.assign({}, props, {\n' +
      '    children: _jsx(_createMdxContent, props)\n' +
      '  })) : _createMdxContent(props);\n' +
      '}\n' +
      'return {\n' +
      '  default: MDXContent\n' +
      '};\n',
    frontmatter: {},
    scope: {}
  }
}

Thanks y’all, I’ll take a look at some of the reported issues with 4.2.1. Note that the previous workaround should likely be removed, and you might need to do a full re-install of next-mdx-remote:

npm uninstall next-mdx-remote && npm install next-mdx-remote

(or whatever the equivalent would be for your package manager of choice)

My package.json:

...
"next": "13.1.1",
"next-mdx-remote": "^4.2.1",
...

I got this working by using:

await serialize(content, {
  mdxOptions: {
    development: process.env.NODE_ENV !== 'production',
  },
})

Works under next dev and next start execution contexts.

Using SSR, then it’s not needed as it turned out. But if you use serialize on client side, then the mdxOptions is needed and have to be switched depending on dev and prod mode.

import Head from 'next/head'
import {useEffect, useState} from 'react'
import {MDXRemote} from 'next-mdx-remote'
import {serialize} from 'next-mdx-remote/serialize'
import {console} from "next/dist/compiled/@edge-runtime/primitives/console";


const components = {
    a: (props) => <a href={props.href}> {props.children} </a>
}

export async function getStaticProps(context) {

    return {
        props: {description: await serialize("[WIKIPEDIA](http://wikipedia.org)")}

    }
}

function MDXContent({children, description}) {

    const [x, sX] = useState(null)
    useEffect(() => {
        ;(async () => {
            if (children)
                sX(await serialize(children, {mdxOptions: {development: true}}))
        })()
    }, [children])

    if (description) {
        return (
            <MDXRemote {...description} components={components}/>
        )
    }
    if (x) {
        return (
            <MDXRemote {...x} components={components}/>
        )
    }
    return null
}


export default function Home({description}) {

    return (
        <>
            <Head>
                <title>Create Next App</title>
                <meta name="description" content="Generated by create next app"/>
                <meta name="viewport" content="width=device-width, initial-scale=1"/>
                <link rel="icon" href="/favicon.ico"/>
            </Head>
            <MDXContent>[WIKIPEDIA](http://wikipedia.org)</MDXContent>
            <MDXContent description={description}/>
        </>
    )
}

It looks there is an entanglement with next-remote-watch - at least in my settings.

In package.json:

"scripts": {
  "dev": "next-remote-watch ../content/pages ./public/static/locales",
}

Running yarn dev (above script) produces TypeError: _jsx is not a function. However, running yarn next dev works as expected with the latest versions of nextjs and next-mdx-remote.

I am not able to use MDXContent in a clientcomp. I am getting TypeError: _jsxDEV is not a function as an error.

It works if you force-downgrade @mdx-js to 2.1.5:

"resolutions": {
	"@mdx-js/mdx": "2.1.5",
	"@mdx-js/react": "2.1.5"
},

thanks, @mmiszy for your temp solution, as maybe someone does not know about selective dependency resolutions and did this workaround, it might produce Certain edge cases that may not work properly since this is a fairly new feature. so please add a TODO: in your code base to upgrade in the future one maintainer adds a hotfix.

worth reading

Here it is: test-app

Thank you!

Hey all,

In addition to the above, I’m seeing the following build errors while trying to import { serialize } from 'next-mdx-remote/serialize':

export 'serialize' (reexported as 'serialize') was not found in './dist/serialize.js' (module has no exports)

This only happens when we try and enable the runtime: 'experimental-edge' Next config property though.

Thought we’d keep all the Next 13 issues in the same place if possible so just tacking on here.

@ramblehead Did you find a current workaround?

@yannickschuchmann not really - just found a versions combination that seems to work for me: next-mdx-remote@4.2.1 and next-remote-watch@2.0.0

It looks there is an entanglement with next-remote-watch - at least in my settings.

Same for me. next dev works. next-remote-watch produces _jsx is not a function

  • next-mdx-remote 4.3.0
  • next-remote-watch 2.0.0_next@13.0.2

@ramblehead Did you find a current workaround?

Hi, thanks for this release! With typing I got 2 errors while testing: with MDXRemote:

'MDXRemote' cannot be used as a JSX component.
  Its return type 'Promise<Element>' is not a valid JSX element.
    Type 'Promise<Element>' is missing the following properties from type 'Element': type, props, key

with compileMdx:

Argument of type '{ source: string; options: { mdxOptions: {}; }; }' is not assignable to parameter of type 'MDXRemoteProps'.
  Property 'compiledSource' is missing in type '{ source: string; options: { mdxOptions: {}; }; }' but required in type 'MDXRemoteSerializeResult<Record<string, unknown>, Record<string, string>>'.

Should I create new issue or is it some error on my side?

I downgraded to

...
"next-mdx-remote": "^4.2.0",
...

and that fixed the issue.

I’m experiencing this same issue, and did some investigating locally. It seems as though the changes made in https://github.com/hashicorp/next-mdx-remote/pull/323 introduced a bug that surfaces when next-mdx-remote is used on Next.js sites in development mode, and not in production builds.

For a temporary fix, I pinned the version of next-mdx-remote I’m using to 4.2.0, and dev mode came right back to mr. #323 is a small change, but i don’t know a ton about how react/jsx-runtime is meant to work in dev vs prod mode, so I haven’t been able to come up with a PR that will fix this package. Would be happy to help if someone understands the issue better than i do!

e: to be clear, I changed my package.json to the following to fix this for now:

"next-mdx-remote": "4.2.0",

Not working for us. We’re on 4.2.1 And we’re not using neither development: falsenor locking the version to "@mdx-js/mdx": "2.1.5"

EDIT:

Also noting that our error is sightly different:

TypeError: _jsx is not a function

instead of

TypeError: _jsxDEV is not a function

Trying to understand why a development build is trying to use _jsx instead of _jsxDEV. This is only happening on development mode. Making a production build seems to be working fine.

It works for me, thanks!

I don’t know if this is related but after upgrading to Next 13.1 we started getting this error:

TypeError: _jsx is not a function
image

Note: We’re not using the app directory anywhere yet.

EDIT:

Here’s what the compiled source looks like

I can say that I still get the same error

@j471n sadly, I have no clue. Let’s see if the maintainers reply.

For anyone else who is blocked by this, you can create a wrapper component (outside of the /app folder) like so […]

This worked for me (new appDir with SSG), thanks @mikewheaton !

I can’t even get it to work using the workarounds pasted above.

When I try, I get Error: Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead..

Does anybody already have a repo they’re willing to share that works (Next 13 + app directory + MDX), even if its using a workaround?