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

Suggestion: Example for MDXProvider with mdx-bundler for component substitution #195

Closed Kuan-Ying closed 7 months ago

Kuan-Ying commented 1 year ago

Thank you for your work. This library is especially helpful for my work because I cannot setup Webpack for MDX compiler within my project, which uses an open-source framework and I have no control of the configs.

I have a minor suggestion for an opportunity to add an example for Component Substitution with MDXProvider.

Relevant code or config

MDX files

Child.mdx
<Text>Awesome!</Text>
Parent.mdx
import Child from './Child.mdx';
<Child />

Server

const { code } = bundleMDX({
  source,
  mdxOptions(options: Record<string, any>) {
      return {
        ...options,
        providerImportSource: '@mdx-js/react',
      };
  }
});

UI

export const MDXComponent: React.FC<{
  code: string;
  frontmatter: Record<string, any>;
}> = ({ code }) => {
  const Component = useMemo(
    () => getMDXComponent(code),
    [code],
  );
  return (
    <MDXProvider components={{ Text: ({ children }) => <p>{children}</p> }}>
      <Component />
    </MDXProvider>
  );
};

What you did

I'm following MDX provider from the official MDX guide, and I would expect Parent.mdx can be compiled and run by React successfully.

What happened

It actually throws the following error in run-time:

Expected component `Text` to be defined: you likely forgot to import, pass, or provide it.

Problem description

It is unclear why the components are not passed to downstream MDX files. The Component Substitution section in this repo only gives an example to pass components to Component, which is cumbersome to pass down components if you have nesting MDX files.

After spending couple of hours diving into the unminified genereated code and source code of @mdx-js/es-build, I think I found the root cause.

When providerImportSource: '@mdx-js/react', the generated JS bundle merges the components from @mdx-js/react's useMDXComponents with the components from the props. If we don't specify useMDXComponents in the globals, mdx-bundler will ship useMDXComponents as well as React.createContext to the bundle.

Now when the app render MDXComponent, MDXProvider has its own useMDXComponents and uses a different version of React.createContext. Because MDXProvider cannot alter the React.context for components in the MDX bundled code, components cannot be accessed in nested MDX files unless you manually pass components down.

Suggested solution

I think it would be nice to have a section in the document for the following solution:

Server

const globals = {
  '@mdx-js/react': {
    varName: 'MdxJsReact',
    namedExports: ['useMDXComponents'],
    defaultExport: false,
  },
};
const { code } = bundleMDX({
  source,
  globals,
  mdxOptions(options: Record<string, any>) {
      return {
        ...options,
        providerImportSource: '@mdx-js/react',
      };
  }
});

UI

import { MDXProvider, useMDXComponents } from '@mdx-js/react';
const MDX_GLOBAL_CONFIG = {
  MdxJsReact: {
    useMDXComponents,
  },
};
export const MDXComponent: React.FC<{
  code: string;
  frontmatter: Record<string, any>;
}> = ({ code }) => {
  const Component = useMemo(
    () => getMDXComponent(code, MDX_GLOBAL_CONFIG),
    [code],
  );
  return (
    <MDXProvider components={{ Text: ({ children }) => <p>{children}</p> }}>
      <Component />
    </MDXProvider>
  );
};
ProchaLu commented 8 months ago

@Kuan-Ying Thanks for this!!

I will open a PR for this for @kentcdodds to review

github-actions[bot] commented 3 months ago

:tada: This issue has been resolved in version 10.0.3 :tada:

The release is available on:

Your semantic-release bot :package::rocket: