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

Support rendering into comment #17547

Closed
dawidgarus opened this issue Dec 8, 2019 · 12 comments
Closed

Support rendering into comment #17547

dawidgarus opened this issue Dec 8, 2019 · 12 comments

Comments

@dawidgarus
Copy link

Do you want to request a feature or report a bug?
Feature

What is the current behavior?
Currently there's isn't any way that I'm aware of to render a component in the middle of element without creating a wrapper element.
For example, let's consider this pure html fragment:

<div>
  <header>header</header>
  <!-- render MyComponent here -->
  <footer>footer</footer>
</div>
function MyComponent() {
  return <section>hello world</section>;
}
React.render(<MyComponent/>, /* what to insert here? */);

You could create DocumentFragment, but after you insert it to the DOM calling render again will generate error:

Warning: render(...): It looks like the React-rendered content of this container was removed without using React. This is not supported and will cause errors. Instead, call ReactDOM.unmountComponentAtNode to empty a container.

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn't have dependencies other than React. Paste the link to your JSFiddle (https://jsfiddle.net/Luktwrdm/) or CodeSandbox (https://codesandbox.io/s/new) example below:

What is the expected behavior?
It would be nice to have on option to pass a Comment node to render function (and similar functions). In such cases, the react elements should be rendered next to the comment, which will work as an anchor to for the rendered content.
Calling:

React.render(<MyComponent/>, queryForCommentNode());

would result in following DOM structure:

<div>
  <header>header</header>
  <!-- render MyComponent here -->
  <section>hello world</section>
  <footer>footer</footer>
</div>

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
16

@vkurchatkin
Copy link

Is there some particular use case for this feature?

@dawidgarus
Copy link
Author

@vkurchatkin In my particular use case, I'm trying to port a design system so it could be used with different frameworks. I've created react wrapper components for my existing components written using lit-html. This feature is would allow me to use simulate slots and project component's children inside lit-html template:

html`<div>Project here: ${slotDirective()}</div>`

I've figured that the slotDirective could put a comment in a place where I want to project content and I could do:

const el = React.createElement(React.Fragment, {}, props.children);
ReactDOM.render([el], theCommentNode);

to render the children in that place.
And no, I'm not using custom elements and I want to avoid it - shadow DOM encapsulation would require rewriting a lot of style.
But it's not only the slots. I'm looking for a cross-framework way to pass arbitrary templates as the component properties.

To be fair, React is not my strongest suit and I'm not much qualified to find other use cases. Perhaps this feature could also be used with portals to also avoid creating wrappers.
But if anyone knows how to achieve something similar, I would really appreciate it.

@kunukn
Copy link
Contributor

kunukn commented Dec 9, 2019

You could use JS to create the nodes you need in the DOM.

Here is a quick PoC of finding the specific text node, create a div and insert after it, then ReactDOM.render into that div.

https://codesandbox.io/s/render-react-app-after-a-text-node-5hhkc

<div>
  <header>header</header>
  <!-- render MyComponent here -->
  <div class="react-app"></div>   <--- div created by JS and React is mounted here
  <footer>footer</footer>
</div>

Maybe this could work?

@dawidgarus
Copy link
Author

@kunukn Well that's what I want to avoid - creating wrapper divs. In the example you provided the actual DOM is:

<div>
  <header>header</header>
  <!-- render MyComponent here -->
  <div class="react-app"><!-- don't want this -->
    <section>hello world</section>
  </div>
  <footer>footer</footer>
</div>

Clearly you can see how that can be undesirable behavior. Not only it creates unnecessary element, but it can mess with layout and css.

@kunukn
Copy link
Contributor

kunukn commented Dec 9, 2019

My best approach is some JS creativity.

function App() {
  return (
    <>
      {/* <section> is omitted */}
      Hello world
      {/* </section> is omitted */}
    </>
  );
}

bootstrapReactApp(
  { renderAtComment: "render MyComponent here", nodeType: "section" },
  { className: "app" },
  App
);

This generates the desired DOM structure.

<div>
  <header>header</header>
  <!-- render MyComponent here -->
  <section class="app">hello world</section>
  <footer>footer</footer>
</div>

Demo example using button and state here.
https://codesandbox.io/s/render-react-app-after-a-text-node-2-eft48

@dawidgarus
Copy link
Author

@kunukn Thanks for the help, but what if there are multiple sections? I cannot make that assumption that there will be only one wrapping element. In fact, I can tell you for sure that I'm expecting cases when there could be more:

<my-input>
  <i class="icon-search" style="order: -1" />
  <i class="icon-keyboard" />
  <i class="icon-clear" />
</my-input>

Which projected inside component should generate in DOM something like:

<my-input>
  <div class="label"></div>
  <div class="container" style="display: flex">
    <input />
    <i class="icon-search" style="order: -1" />
    <i class="icon-keyboard" />
    <i class="icon-clear" />
  </div>
</my-input>

For example, lit-html can render template into DocumentFragment and update it even after the fragment was inserted into other element:
https://stackblitz.com/edit/typescript-skuhff?file=index.ts

@embeddedt
Copy link

The problem I see here is that React has to take over the container for all components and erase all non-React siblings.

For an example, consider the following HTML.

<div id="#react-container">
    <div>completely unrelated to React</div>
</div>

If ReactDOM.render were called using #react-container, the unrelated div inside would be removed from the DOM.

If you really need this kind of behavior, I think you would have to invert the logic and put React at the top of your tree and lit-html inside it. That way React still maintains control of the root container.

@bjrmatos
Copy link
Contributor

bjrmatos commented Dec 10, 2019

@dawidgarus i think this is already supported, although not documented (probably because it is still marked as unstable). here is the PR that added support for this, you can see it was done long time ago and it works very similar to what you are requesting, the only difference is that the component rendering result is added before the comment node, not after, in any case i guess it does not matter for you. the other requirement for this feature to work is that the comment node is exactly the following: <!-- react-mount-point-unstable -->, and again i guess this is not a problem for your case.

you can see a live example of that here

hope it helps you.

@dawidgarus
Copy link
Author

@bjrmatos Thanks, that's exactly what I need. Although, I'm not really comfortable with using undocumented APIs and magic constants like 'react-mount-point-unstable'.
Even TypeScript typings say you cannot use comment node in render function.
Is there any chance it will become official?

@threepointone
Copy link
Contributor

I believe we’re going to remove that api in the near future. We only use it inside fb, and we’re moving away from it.

@dawidgarus
Copy link
Author

dawidgarus commented Mar 6, 2020

@threepointone That's unfortunate. There are some really cool use cases for this.
For example, I'm using it to create React adapter for Angular components (something like Angular elements but for React component as target, not custom element) to get content projection working: I project a comment node inside Angular component and then render children to that comment node. I've actually got most of Angular material library working in React.
I'm sure people will come up with other use cases, because it add some versatility, especially when it comes to integration of React with other DOM manipulation libraries and porting design systems.
I hope the React team will revisit the idea or add similar feature, like ability to render into DocumentFragment and update it after it was inserted into other element.

@burner03
Copy link

Seems like this option has been removed with the addition of createRoot & hydrateRoot. Are there any other options to support rendering into a comment instead of a dom element?

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

7 participants