-
Notifications
You must be signed in to change notification settings - Fork 46.9k
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 for changing a Portal's container without remounting children? #12247
Comments
We are also hitting against this issue in |
We have found that moving a component to a portal, especially with lots of children, has a significant performance penalty |
@alexreardon Can you describe in more detail what you're trying to do with moving portals? It's not entirely clear to me. Maybe some examples of the tree before and after? |
This sounds like the same issue React has in general with when you want to move things from one parent to another. The presence of portals makes this novelly interesting, the DOM element is not one React created but one the user gave to react. In fact this sounds the exact same, because the portal interface isn't one you do something like The issue for reparenting is #3965 and has been an issue for nearly 3 years. Recently I started an RFC for a possible reparenting API: reactjs/rfcs#34 This API should work with Portals as well. You can |
I will give a bit more context @gaearon. When a user starts a drag interaction we use As a part of our React 16 upgrade we are hoping to move the dragging item into a React portal so that it does not matter what styles your parents have (including We are doing this: if (!shouldUsePortal) {
// For the purpose of testing I kept the structure the same for both render paths
return (
<React.Fragment>
{child}
{null}
</React.Fragment>
);
}
// When dragging we put the Draggable into a portal
const inPortal: Portal = ReactDOM.createPortal(
child,
// simply gets our portal dom element
getPortal(),
);
return (
<React.Fragment>
{inPortal}
<Placeholder placeholder={dimension.placeholder} />
</React.Fragment>
); When the component is rendered into a portal it unmounts the component and mounts it in the new location. We are finding that this has quite a large cost. This cost is extremely high when the dragging item being but into a portal has a lot of children. |
If i'm not mistaken the The only reason i understand that react does this is the same reason that two elements with differing types i.e The behavior suggested by @danielran looks like a reasonable way to solve this pattern, albeit it might be a breaking change if the current heuristic is expected/documented behavior. |
Correct, there is a (not well documented) key argument to createPortal, this is simply passed as the key prop to the resulting React element. This is likely just so that you can use it in an array. i.e. Though I suppose in this case React does have a bit more information than just child and container. The portal is a normal react element with a local key and it is rendered into a parent in react the same way a normal Although, while that's true one could argue that saying In fact I can think of a possible deoptimization as a side effect of this: if ( isA ) {
return React.createPortal(<div className='foo-a'>...some complex page...<A /></div>, containerA);
} else {
return React.createPortal(<div className='bar-b' style={style}>...some complex page...<B /></div>, containerB);
} Current likely behaviour when going from isA to not-isA:
Behaviour for createPortal calls without use of the undocumented
And while we could technically handle this, in the very specific case where you render a portal and then change the container of the portal, I'm not sure it's at all useful to implement. It only covers a single use case, it doesn't help with the (IMHO likely more common) desire of being able to move things in/out of a portal, and unlike the other use cases it doesn't solve it is trivial to workaround in user-space. The OP suggestion seems to be to optimize this general idea: // html
<div id="rootA"></div>
<div id="rootB"></div>
// render
React.createPortal(<div class="container"><Foo /></div>, condition ? rootA : rootB); So that when rootA becomes rootB is changed React does a bunch of work to move the dom nodes. However the general idea of Portals is to ask React to do less and let you control the dom outside of React instead of asking React to do more dom work. As you control the dom that the portal is rendered into, you can trivially change this general idea to instead work like this. // html
<div id="rootA"></div>
<div id="rootB"></div>
// mount
this.setState({container: document.createElement('div')});
// update
(condition ? rootA : rootB).appendChild(state.container);
// render
React.createPortal(<Foo />, state.container); And it requires no work in React or possible negative side effects in order to get things to working. And we can focus on the larger scope of reparenting issues. |
I'd like to pile on another pretty specific use case that would be very nice to solve with portals, since currently I have to do a whole bunch of manual DOM wrangling I'd like to not do. Basically, I'm rendering a hidden video element at the top of my element hierarchy, and at different points I want to be able to display the video in different positions on the page. Due the the highly logic-coupled-to-view nature of the Similar to @alexreardon I don't want to be forced to rely on style-based repositioning as I'm building a library and asking users to use absolute positioning to solve their layout problems seems wrong. So for me it's a requirement that I can move the same element around to different places in the DOM (moving the element will pause my video forcing me to call So why do I feel that I want portals to solve this problem for me? Well if not, then my only choice is to render my Does anyone know a workaround? |
@gaearon would you accept a pull request for this? |
@benwiley4000 I'm doing some research to solve a similar problem. Have you made any progress on this? Has your workaround been effective? |
We had a similar problem and solved it in userland with a combination of portals, context, and refs. It's a bit convoluted but it works. The idea is that you have state and a "node manager" live at the top. Your leaf component, when it mounts, passes its "leaf div" up the context to the "node manager". In return, the "node manager" gives the leaf component a node to portal into. That node would always be the same. Node manager would create it once. Initially, node manager would attach it to the leaf's own "leaf div". But it can also detach it and attach it somewhere else without recreating the element. From leaf's point of view, it always renders the portal into the same node. But the thing at the top can move that node from an actual div (originally in the leaf) to any other div on the page. Instead of reparenting the portal itself, we always portal into the same node, but that node itself is moved in the document imperatively wherever we need. |
@mmartinson there's no real "workaround" except to not use React for this. I'm doing stuff slightly differently than described above.. but basically I render a div container in a component somewhere, then call this callback with a ref to the div go have the video rendered there. Another video container can steal it later if wanted. I'm doing all of that with raw DOM.. as far as React is concerned, the video element doesn't exist, but something convenient I learned was that React won't mess with children manually-added to empty elements, even on re-render. So no need for the |
There is a workaround — I just described it. We use it, and it works. The description is a little brief but I can try to create a fiddle if someone gives me a base to work on. |
Oh wait, I guess you described roughly the same thing. |
@gaearon hm what's the advantage of using the portal here instead of just appendChild? I guess that makes sense for a more complex bit of markup you'd want to move around. Is there any code you could share? |
Our conversation has a bit of a race condition going on 😄. Yes I'd love to see some example code! |
Ping @sompylasar, I think you had some demo that wasn't using any proprietary code? |
@gaearon Using |
I think I've got it working. Thanks all for the input. |
@mmartinson using portals? |
@mmartinson hey, just wanted to poke again on this.. did the solution you found use portals? |
Hey @benwiley4000. Ya the solution I found does use portals, though I'm not sure whether the use is superfluous or not with the other imperative changes that are being done. My understanding of the internal mechanics are admittedly limited. I have a hidden media manager component with ref that is passed to the media component when it renders. Media component is rendered with To change position, the media manager uses the props in
This seems to do it for me, though I'm not certain if there are any race conditions lurkers in there that I'm just getting lucky with. |
@gaearon @benwiley4000 I hit this too & fixed it for myself. Rather than put together an example, I've turned the basic pattern you're describing into a library you can use to solve this properly. It's effectively the concept defined above, with a few additions, and lets you define some content in one place, render & mount it there once, then place it elsewhere and move it later (potentially out of the page entirely, and back) all without remounting or necessarily rerendering. I'm calling it reverse portals, since it's the opposite model to normal portals: instead of pushing content from a React component to distant DOM, you define content in one place, then declaratively pull that DOM into your React tree elsewhere. I'm using it in production to reparent expensive-to-initialize components, it's working very nicely for me! Repo is over here: https://github.com/httptoolkit/react-reverse-portal. Let me know if it works for you! |
In my case I only needed to update the component on url and component change basis, so I used |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contribution. |
Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please create a new issue with up-to-date information. Thank you! |
Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please create a new issue with up-to-date information. Thank you! |
(This is related to #3965.)
I'm working on a project with a lot of globally unique component instances. (In other words, their
key
s are essentially database IDs.) It also has DnD functionality. For reordering, everything is fine, but moving an instance from one parent to another causes a complete re-render of the instance.Instead of moving the nodes around myself, I was thinking of using Portals. Each instance has a prop for which element ID they should render inside. Then, to reparent them, it'd be a simple case of passing a different element ID. (I'm using Redux, so it'd just be a
string
in the state.)However, changing a Portal's container node also causes a complete re-render of everything passed to
ReactDOM.createPortal
. (See this CodePen.)Would it be possible to skip this re-rendering and effectively move the Portal contents instead?
The text was updated successfully, but these errors were encountered: