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

Generated JSX should not define component on rerender #2029

Closed
4 tasks done
remcohaszing opened this issue May 5, 2022 · 8 comments · Fixed by #2062
Closed
4 tasks done

Generated JSX should not define component on rerender #2029

remcohaszing opened this issue May 5, 2022 · 8 comments · Fixed by #2062
Labels
🏁 area/perf This affects performance 💪 phase/solved Post is done

Comments

@remcohaszing
Copy link
Member

Initial checklist

Affected packages and versions

^2.0.0-rc.2 (#1810)

Link to runnable example

No response

Steps to reproduce

Run the following script:

import { compile } from '@mdx-js/mdx';

const { value } = await compile('hello', { jsx:true })

console.log(value)

The jsx option is there for readability of the output. The issue applies if it’s either true or false.

Expected behavior

The output doesn’t define a component inside a component. This is a React anti-pattern, because it causes unnecessary rerenders

The following Google search yields some articles that explain why this is a bad idea in React: https://www.google.com/search?q=react+create+component+inside+component. Example article: https://dev.to/borasvm/react-create-component-inside-a-component-456b. The same principles apply to Preact.

Earlier the output was ok:

/*@jsxRuntime automatic @jsxImportSource react*/
function MDXContent(props = {}) {
  const _components = Object.assign({
    p: "p"
  }, props.components), {wrapper: MDXLayout} = _components;
  const _content = <><_components.p>{"hello"}</_components.p></>;
  return MDXLayout ? <MDXLayout {...props}>{_content}</MDXLayout> : _content;
}
export default MDXContent;

Alternative output that’s also ok for React/Preact, although I recall it’s troublesome for some frameworks:

/*@jsxRuntime automatic @jsxImportSource react*/
import { Fragment as _Fragment } from 'react/jsx-runtime.js'
function MDXContent(props = {}) {
  const _components = Object.assign({
    p: "p"
  }, props.components), {wrapper: MDXLayout = _Fragment} = _components;
  return <MDXLayout><_components.p>{"hello"}</_components.p></MDXLayout>;
}
export default MDXContent;

Actual behavior

The output does define a component inside a component, which causes unnecessary rerenders.

/*@jsxRuntime automatic @jsxImportSource react*/
function MDXContent(props = {}) {
  const {wrapper: MDXLayout} = props.components || ({});
  return MDXLayout ? <MDXLayout {...props}><_createMdxContent /></MDXLayout> : _createMdxContent();
  function _createMdxContent() {
    const _components = Object.assign({
      p: "p"
    }, props.components);
    return <_components.p>{"hello"}</_components.p>;
  }
}
export default MDXContent;

Runtime

Node v16

Package manager

npm v8

OS

Linux

Build and bundle tools

Other (please specify in steps to reproduce)

@wooorm
Copy link
Member

wooorm commented May 5, 2022

Unfortunately it’s required for context-based components passing inside MDXLayout to work.
Before, components could not be set in the context of an MDXLayout. After, it can.
If you don’t pass an MDXLayout, the function is called immediately, not as a component, so it does not affect users that don’t use layouts.

@remcohaszing
Copy link
Member Author

I see the issue you’re describing.

I think the following would solve both this issue and the issue you described:

/*@jsxRuntime automatic @jsxImportSource react*/

function _createMdxContent(props) {
  const _components = Object.assign({
    p: "p"
  }, props.components);
  return <_components.p>{"hello"}</_components.p>;
}

function MDXContent(props = {}) {
  const {wrapper: MDXLayout} = props.components || ({});
  return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props} /></MDXLayout> : _createMdxContent(props);
  }
}
export default MDXContent;

@wooorm
Copy link
Member

wooorm commented May 5, 2022

Perhaps! It looks good but I'm not 100% it'll work

@0phoff
Copy link
Contributor

0phoff commented Jun 10, 2022

I think this inner component also blocks libraries that want to traverse and modify the react element tree (MDX deck, mdxp).
When I print the generated MDXContent component in the console, it shows no children in it's props.

Since I would love to update MDXP to MDX2, I am willing to look into this issue and submit a PR. I imagine I would need to look into the recma plugin of @mdx-js/mdx. Do you have any pointers to get me started? I have some experience with remark/rehype but never looked at recma.

@wooorm
Copy link
Member

wooorm commented Jun 10, 2022

I don’t believe traversing the react tree is the way to go, generally. We have ASTs and they’re much better.

The approach proposed above looks good. It waits for someone to work on it, indeed, so if that’s you, 👍.
It would go in core, probably around here:

function createMdxContent(content) {

If you have experience with ASTs, then you should be able to figure out how the JS AST works, looking at ASTExplorer and the existing code. Let me know if you have questions

@0phoff
Copy link
Contributor

0phoff commented Jun 10, 2022

I don’t believe traversing the react tree is the way to go, generally. We have ASTs and they’re much better.

I completely agree and was looking for a way to perform this as a rehype/recma plugin, but couldn't figure out how to do the following:

Users of my library can mark components as being a "slide layout component". If such a component is used as the sole component of a slide (between 2 <hr>), then I consider that to be a slide. If there are multiple components between the <hr> tags or the component is not defined as a layout, then I wrap the content in a default slide layout.

The way I mark a component as a slide layout is by adding a property MDXPType on the function and thus I think I can only perform this check at runtime. The only way I can think of to do this with unified at build time, is to either pass a list of slide layouts to the plugin or have a rule that slide layout components should always end in "SlideLayout". I don't really like either of these options and thus implemented my parsing at runtime in the client 😅

@wooorm
Copy link
Member

wooorm commented Jun 10, 2022

This sounds like a whole, unrelated, problem. Perhaps you can open a discussion and share much more about what you want to do.

I don’t understand why MDXPType is added instead of the new MDX AST nodes. You can walk the AST, “split” on thematic breaks, and check if there’s only one thing in them, in which case you wrap content into a JSX element.

wooorm pushed a commit that referenced this issue Jun 17, 2022
Closes GH-2029.
Closes GH-2062.

Reviewed-by: Titus Wormer <[email protected]>
Reviewed-by: Christian Murphy <[email protected]>
@wooorm wooorm added 🏁 area/perf This affects performance 💪 phase/solved Post is done labels Jun 17, 2022
@shepmaster
Copy link

I think this inner component also blocks libraries that want to traverse and modify the react element tree (MDX deck, mdxp).

I had a similar problem, also with a slide deck implementation. I added a comment elsewhere describing what I did.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🏁 area/perf This affects performance 💪 phase/solved Post is done
Development

Successfully merging a pull request may close this issue.

4 participants