mdx: MDX is not compatible with @babel/plugin-transform-react-inline-elements

Subject of the issue

When combined with @babel/plugin-transform-react-inline-elements, MDX often not picking up the custom components provided through MDXProvider and renders the default components instead. This is probably due to heavy optimizations this babel plugin performs on react components. Removing the plugin from babel configuration fixes the issue.

This plugin is only typically used in production and so the problem will not manifest during development or in tests. Therefore this breakage may slip unnoticed to production. In fact, I just shipped a broken version yesterday.

Wouldn’t it be nice for MDX to support configurations with this babel plugin?

Your environment

  • OS: Ubuntu 20.04

  • Packages:

    {
      "dependencies": {
        "@mdx-js/react": "1.6.21",
        "@next/mdx": "10.0.1",
        "next": "10.0.1",
        "react": "16.14.0",
        "react-dom": "16.14.0",
        "styled-components": "5.2.1"
      },
      "devDependencies": {
        "@babel/plugin-transform-react-inline-elements": "7.12.1",
        "@mdx-js/loader": "1.6.21",
        "prettier": "2.1.2"
      }
    }
    
  • Env:

    node: v14.15.0
    yarn: 1.22.10
    chromium: 83.0.4103.116
    

Steps to reproduce

I created a small (but not minimal) example demonstrating the issue here: https://github.com/ivan-aksamentov/repro-mdx-inline-elements

It’s based on Next.js and also contains styled-components (but none of this matter, see below).

The points of interest are:

In order to reproduce the bug, run the production version of this app with:


yarn install
next build && next start

and navigate to localhost:3000. You will see something like this: bad

Note how all of the components are styled with plain useragent stylesheet (meaning most of the custom components ARE NOT picked up). Inspect the HTML code for the “link” in dev tools and note that it DOES have target="_blank" rel="noopener noreferrer" attributes (meaning that the custom LinkExternal component IS picked up). Also note how >>>> LinkExternal <<<<< is printed in build console (in terminal), but not >>>> H1 <<<<< (these are console.log() statements in the corresponding components).

Go to babel.config.js and comment-out the line containing '@babel/plugin-transform-react-inline-elements'. Cleanup, rebuild and restart the app.

rm -rf .build out .cache .next
next build && next start

It should look like this now: good

Note that all cusom components now work correctly: styled components’ classnames are present, h1’s text is replaced, backrgounds and margins are correctly styled, both console statements are printed in the build log, as expected. The LinkExternal component also still works as before. Reversing the change in babel.config.js reverts the fix.

Styled components don’t play a role here, I just wanted to show that they are working as well, because they are important for my usecase. With these components, babel-plugin-styled-components, and associated npm packages removed the issue still persists.

Expected behaviour

It is expected that the custom components are picked up, whether the @babel/plugin-transform-react-inline-elements is used or not

Actual behaviour

Custom components are not picked up when @babel/plugin-transform-react-inline-elements is used

Discussion

Let’s generate a readable bunde code and see what is changing when adding/removing the plugin. I added two Next.js plugins withoutMinification() and withFriendlyChunkNames(), which remove code minification and hashes from filenames in production build.

I did the following experiment:

  • prepared output directories:

    rm -rf compare/{good,bad}/**
    mkdir -p compare/{good,bad}
    
  • with babel plugin DISABLED, produced static build for “GOOD” version (static build has exactly the same issues as normal build, but it’s easier to make sense of files it produces):

    rm -rf .build out .cache .next
    next build && next export
    cp -r .next/static/chunks/* compare/good/
    
  • with babel plugin ENABLED, produced static build for “BAD” version:

    rm -rf .build out .cache .next
    next build && next export
    cp -r .next/static/chunks/* compare/bad/
    
  • compared the resulting directories with webstorm:

    detach webstorm diff compare/{good,bad}
    

    the only difference seems to be in pages/index.js

  • generated a diff file for pages/index.js

    diff -u compare/{good,bad}/pages/index.js > compare/index.js.diff
    
  • one could also use diff-so-fancy to see the pretty diff in terminal:

     diff -u compare/{good,bad}/pages/index.js | diff-so-fancy
    

You can find the results in compare/, directory, inluding the compare/index.js.diff

I was not able to make sense of the diff yet.

Unrelated to diff, but interestingly, the reason LinkExternal works seems to be the fact that it uses (renders) children props. Adding children to H1 component also fixes the h1 rendering. So props seems to be influencing the code optimizations in question. However, side effects, like console.log() don’t seem to be preserved (notice how they are not printed during build) Sadly, this workaround will not work for styled components.

Related issues in the community

styled-components had a seemingly similar issue, and were able to solve it, while emotion given up on this: link1, link2.

There is also a similarly useful plugin, @babel/plugin-transform-react-constant-elements. So far it does not seem to cause any breakage. However, it woth keeping an eye on it as well.

Possible workarounds

  • remove @babel/plugin-transform-react-inline-elements, paying extra runtime performance and bundle size cost.

  • use children prop in custom components - does not work for many components, like styled-components, or components that are not meant to have children.

Update:

Perhaps webstorm’s diff is a bit more readable (left side is “good”, right side is “bad”):

Looks like something is going on with this added function, as well as with null and void 0 arguments on call site. Still cannot tell what’s wrong. For example, console.log() calls are present in H1 on both sides, but are not working on the right side, while working on the left side.

Update 2:

It may make more sense to examine the output of the normal next build in .next/static/chunks (rather than of static export).

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 16 (10 by maintainers)

Commits related to this issue

Most upvoted comments

Thanks! I have no clue why this is happening, maybe ask the babel folks?

Btw, most of the diff you’re seeing is because the heading component uses an arrow, where a is just a function, index.jsx like so:

import * as React from "react";
import * as ReactDOM from "react-dom";

import { MDXProvider } from "@mdx-js/react";

import Content from "./content.md";

function h1() {
  return <span>!!!heading 1</span>;
}

function a() {
  return <span>!!!link</span>;
}

export default function Index() {
  return (
    <MDXProvider components={{ h1, a }}>
      <Content />
    </MDXProvider>
  );
}

ReactDOM.render(<Index />, document.getElementById("app"));

Produces:

var layoutProps = {};
var MDXLayout = "wrapper";
function MDXContent(_ref) {
  var components = _ref.components,
      props = content_objectWithoutProperties(_ref, ["components"]);

  return createElement(MDXLayout, content_extends({}, layoutProps, props, {
    components: components,
    mdxType: "MDXLayout"
  }), /*#__PURE__*/_jsx("h1", {}, void 0, "Heading 1"), /*#__PURE__*/_jsx("p", {}, void 0, createElement("a", content_extends({
    parentName: "p"
  }, {
    "href": "http://example.com"
  }), "link")));
}

...

function h1() {
  return /*#__PURE__*/src_jsx("span", {}, void 0, "!!!heading 1");
}

function a() {
  return /*#__PURE__*/src_jsx("span", {}, void 0, "!!!link");
}

function Index() {
  return /*#__PURE__*/src_jsx(esm_MDXProvider, {
    components: {
      h1: h1,
      a: a
    }
  }, void 0, /*#__PURE__*/src_jsx(MDXContent, {}));
}
react_dom["render"]( /*#__PURE__*/src_jsx(Index, {}), document.getElementById("app"));

…and I don’t know what babel-plugin-transform-react-inline-elements is basing the difference in the a and h1 on? 🤷‍♂️