kentcdodds / mdx-bundler

🦤 Give me MDX/TSX strings and I'll give you back a component you can render. Supports imports!
MIT License
1.78k stars 75 forks source link

Unable to use state hook in MDX #191

Open js-egton opened 2 years ago

js-egton commented 2 years ago

Relevant code or config:

mdxSource is the code returned from bundleMDX with the following plugins:

{/* Main render component */}
const MDXComponent = useMemo(() => getMDXComponent(mdxSource), [mdxSource]);
let componentsToLoad = {
    Example,
    JSXHighlighter,
    (etc)
};

return (
  <MDXComponent components={componentsToLoad} />
);

{/* MDX file */}
import { useState } from "react";
export const [dropdownMenuOpen, setDropdownMenuOpen] = useState(false);
...
<Button onClick={() => setDropdownMenuOpen(!dropdownMenuOpen)}>Button</Button>

What you did:

Added a state hook (with useState) inside MDX file, then triggered a state update on the rendered output.

What happened:

When loading the page initially, Next.JS outputs the following warning to the terminal:

Warning: Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. You can only call Hooks at the top level of your React function. For more information, see https://reactjs.org/link/rules-of-hooks

After clicking the button to trigger the state update, the rendered page errors out with the following:

Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement.

Reproduction repository:

N/A

Problem description:

By using useMemo to create the <MDXComponent> constant, the useState hook is being nested; this goes against React's Rules of Hooks and causes the code to throw an error. Removing the useMemo call suppresses the terminal error, but the page then becomes unresponsive after triggering a state update (presumably as is tries to remake MDXComponent over and over).

Suggested solution:

Unknown.

hahnbeelee commented 2 years ago

React hooks can only be made within React components. The contents of an MDX file are not inherently a React component. Your example looks like it'd be more suitable if you made a separate Button component in a jsx file then imported it into your MDX file to use it.

js-egton commented 2 years ago

Your example looks like it'd be more suitable if you made a separate Button component in a jsx file then imported it into your MDX file to use it.

The Button already is a separate component 🙂 However it cannot be imported into the MDX directly because it is not a default export of the Button JSX file, and this package does not support import { X, Y } from "Z" style imports (as far as I could tell), hence the use of the components prop on the <MDXBundler> component to pull external components into the MDX.

React hooks can only be made within React components. The contents of an MDX file are not inherently a React component.

I'm aware that MDX != component, and that this is a fairly odd edge case usage to be honest! For the moment we have a workaround that doesn't use MDX, and it's working out okay for us so far 👍