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

evaling code breaks if code contains import statements with sources that should be resolved by the bundler #9

Open
eps1lon opened this issue Feb 25, 2025 · 4 comments

Comments

@eps1lon
Copy link

eps1lon commented Feb 25, 2025

This is based off of vercel/next.js#76395

The concrete case used React.createElement instead of just JSX to construct elements. However, the same issue occurs if any other React API is being used.

Next.js applies aliases to certain import sources. However, this does not apply to code dynamically evaluated like eval or Reflect.construct. Since next-mdx-remote-clientuses Reflect.construct to run code, imports to React will resolve to the wrong version of React. In this case it's the installed, Client React. Mixing different versions of React or Server and Client React in the same component is not supported.

@talatkuyuk
Copy link
Contributor

I think and insist that the issue is not related with next-mdx-remote-client, but Next.js or react@canary itself. Follow the vercel/next.js#76395

@eps1lon
Copy link
Author

eps1lon commented Mar 3, 2025

It has the same cause as React.useId throwing in code imported from MDX. That goes back to Next.js 14.0 and React 18.

React.createElement just happened to work despite this issue. Now React.createElement isn't working either in latest Next.js. But not because of a change in Next.js but because eval et al. cannot be used on code that imports React. You have to pass in the React instance.

Example that is already broken in Next.js 14 for the same underlying reason:
page.js

import { Suspense } from "react";
import { MDXRemote } from "next-mdx-remote-client/rsc";

export default async function Page() {
  const source = "# Import\nimport Test from './Test.mjs';\n\n<Test />";

  return (
    <Suspense fallback="Loading...">
      <MDXRemote
        source={source}
        options={{
          mdxOptions: {
            baseUrl: import.meta.url,
          },
        }}
      />
    </Suspense>
  );
}

Test.mjs

import * as React from "react";

export default function Test() {
  React.useId();
  return "Hello, Dave!";
}

@talatkuyuk
Copy link
Contributor

talatkuyuk commented Mar 6, 2025

Hi @eps1lon, I confirmed that React.useId() throws TypeError: Cannot read properties of null (reading 'useId').

Seems that the imported component within MDX couldn't find the React during construction via Reflect.construct. But, React.createElement works interestingly.

Even if we don't use any React APIs/hooks, just return <div>Hi</div> causes an error unexpected token < during function construction.

A React component within an .mjs file can return a JSX element and use hooks like React.useId(), as Next.js' bundling system supports it. However, seems that this doesn't apply during function construction, as it doesn't trigger Next.js' bundling process.

I will try to include React instance to the function construction arguments; and will see it works.

But, I am trying to understand, why ? What is going on during function construction ?

  • when I use React.useId(), the error says the React is null [TypeError: Cannot read properties of null (reading 'useId')]
  • when I use React.useState(), the error says the React is o, but useState() is not a function [TypeError: o.useState is not a function] during build; but throws [TypeError: Cannot read properties of null (reading 'useState')] when running in dev and with "use client" directive during build.
  • when I use React.createElement(), there is no error.
    Isn't it interesting ?

@talatkuyuk
Copy link
Contributor

talatkuyuk commented Mar 7, 2025

I updated and published the new version of next-mdx-remote-client v2.1.0

Now, [email protected] works with [email protected] without problem.

Basically, I passed the React instance into Reflect.construct as an argument, along with JSX runtime. See the code.

If the MDX content doesn't contain any import declarations for React components, you don't need to do any additional things. But, if the MDX content contains a import declaration for a React component, then you need to use a recma plugin in recmaPlugins option. You have it 😄 . I created a new recma plugin called recma-mdx-import-react in order to get the React instance in the compiled source and inject it as property into the imported React components.

Basically, recma-mdx-import-react updates the compiled source to ensure that the React instance is only available in the imported components:

// ...
+ const React = arguments[0].React;
const {Fragment: _Fragment, jsx: _jsx, jsxs: _jsxs} = arguments[0];
// ...
const {default: Test} = await import(_resolveDynamicMdxSpecifier("./context/Test.mjs"));
function _createMdxContent(props) {
  // ...
  return _jsxs(_Fragment, {
    // ...
-   _jsx(Test, {})
+   _jsx(Test, { React })
    // ...
  })
}

In the example that @eps1lon shared above, importing Test.mjs has already happened, and I see that React instance is available in the Test component. No more getting an error throwing React is null.

Test.mjs

// import * as React from "react"; // no need

export default function Test({ React }) {
  React.useId();
  return "Hello, Dave!";
}

As a conclusion, passing React instance into function construction (in a hacky way via recma-mdx-import-react) has solved a part of the issue.

But, a new problem appeared. I am able to use any react hooks (like React.useState) inside that Test.mjs, but needs "use client" directive in the file. Since the function construction via Reflect.construct runs in isolated environment, the Next.js doesn't recognize "use client" directive in that file; and it may need to make whole MDXRemote to be client component which is an undesired situation.

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

No branches or pull requests

2 participants