When writing a component that contains a set of large subtrees that stay relatively the same, but are simply moved around such that React's virtual DOM diffing can't detect the movement, React will end up recreating huge trees it should simply be moving.
For example, pretend blockA
and blockB
are very large structures. They may be made of several levels of children and components. For example one could be the entire page contents and the other the sidebar, while this render()
is the page root.
render() {
var blockA = <div>AAA</div>,
blockB = <div>BBB</div>;
if ( this.props.layoutA ) {
return <div>
<div className="something">{blockB}</div>
<div className="something">{blockA}</div>
</div>;
} else {
return <div>
{blockA}
{blockB}
</div>;
}
}
Because the blocks aren't at the same level React cannot see the relation between these blocks and key
cannot be used to give React any hints. As a result, when layoutA
is changed, instead of the two blocks being moved to their new location the entire page is essentially completely unrendered and then re-rendered from scratch.
I understand why this is the case. It would be far to expensive for React to be able to detect movement of nodes like this.
But I do believe we need a pattern to hint to React that this component has large blocks that may be moved around at different levels.
Note that there may be a component in between the rendering component root and the block. So parent semantics scoped to the nearest component won't work. This'll need owner scoping.
I understand that React is trying to eliminate the need for React.createElement to be used and owner scoping within special attributes interferes with that. So instead of a component scoped key=""
variant I think a method/object style interface kind of like React.addons.createFragment
might work.
This is a tricky problem. You might want to share your input here.
How about such hint:
render() {
var blockA = <div>AAA</div>,
blockB = <div>BBB</div>;
if ( this.props.layoutA ) {
return <div>
<div className="something">{blockB}</div>
<div className="something">{blockA}</div>
</div>;
} else {
return <div>
<div>{blockA}</div>
<div>{blockB}</div>
</div>;
}
}
@vkurchatkin I don't see any hint or change in your version of the example.
@vkurchatkin For two different render
calls, those would be two different React element instances. So this doesn't really help, as React can't āguessā if you're using the same variables inside render
branches or not. Also, in some cases build tools will optimize constant element creation and put it outside render
, so even if such guessing was possible, you'd get false flags for constant elements reused several times inside one rendered tree.
@gaeron well, in this particular case both trees have the same shape, so they will be reconciled just fine, won't they? in other case it should be easy enough to wrap subtrees in components to give React a hint.
@gaearon Yeah, guessing is even trickier than that, because render could create multiple copies of elements (either in a loop or via a helper function), resulting in distinct elements, even though they were created in the same "location" in code.
@dantman I think your best bet (at least medium term) is going to be to hoist the component state up above the tree (rather than relying on component state). See https://github.com/facebook/react/issues/3653#issuecomment-92526513. That will, in general, solve the re-parenting problem from a correctness point of view. Then, the only remaining 'issue' is performance (re-creating DOM instead of re-using the 'moved' markup), but React is pretty fast and I'm guessing that isn't too much of a concern.
@dantman The change is the addition of the divs in the else block of the render function. @vkurchatkin is correct that this should preserve the DOM shape/structure, thus allowing React to better figure out what's going on during reconciliation and re-use the structure. His change is subtle, but correct.
I'd just like to point out that you can do reparenting manually today, if you render the component you want to reparent into a separate non-React node, this node can then be reparented manually anywhere you like (but remember that you may only put it in empty React nodes).
@syranide The caveat is that since you're doing that by rendering into a separate DOM node it doesn't work on the server.
I actually experimented with that recently. I tried getting around the server limitation with a messy setup where I'd render() the node in react for the server, then on the client drop that and re-render into a dom node that can be relocated. Unfortunately I ran into some issues with React stumbling on modified DOM and had to scrap the experiment.
Maybe more real life use cases can add some priority to this issue ...
@kof Could you expand on that? It's a wall of text and I don't see anything that obviously applies, intuitively I don't see any use for reparenting in an implementation of virtual lists?
@syranide is this a good explanation? #7460
Quick summary:
To measure the size of a rendered component invisibly from the user, before we show it to the user, we need to
What we need is:
There's a lot of background with that particular use-case that's specific to the architecture of react-virtualized. Trying to summarize as much as possible: RV needs to know the actual sizes of its elements at some point so it can window things. For elements _ahead_ of the current cursor sizes can be estimated, but for elements _behind_ (above, left-of) actual sizes need to be known (or scrolling would be janky).
The CellMeasurer
HOC helps to just-in-time measure a given row or column. Sometimes the cells it's measuring aren't actually visible (eg if a user quick-scrolls and skips a range or cells, RV needs to measure the ones that were skipped). So CellMeasurer
uses unstable_renderSubtreeIntoContainer
to measure these cells in a hidden div. The same measured cells may later be rendered (depending on which direction the user scrolls).
@kof @bvaughn Ah ok. I see what you mean, although IMHO I would say from a technical perspective reparenting is not the right solution to that problem in the React world. It would require one render for measuring and then immediately scheduling another render to perform reparenting, it works, but it is abusing React and would have unintended side-effects.
If I would quickly suggest the general solution to this problem I would probably say; being able to render React components into nodes without attaching them to the DOM and being able to render raw nodes into the DOM. Both of these can be kind-of accomplished today by performing React.render
into hidden elements and then moving these into the virtual list, but there are some caveats as you're probably aware. However, rendering to nodes has been discussed before and a version of #7361 could make the last part native (not really all that necessary though).
Yes the caveat is the mounting overhead. Somehow we need to avoid second mount and just transfer the element to a different parent.
@kof That's not what I mean, rendering into the hidden element and then moving _that_ into the virtual list. The caveat is that you currently need a wrapper element and manage some of the DOM stuff yourself, there may also be some very subtle differences due to rendering into separate sub-trees.
Another use-case: a pop-out video. Eg, sits in a specific place in the DOM, but when activated it moves to the root element and becomes position:fixed
.
This movement shouldn't impact playback.
The generated keys idea in the gist would work I think. As would some kind of globally-unique-key
attribute.
@jakearchibald I'm pretty sure it does affect playback, iframes reload and audio stops if you just as much as hint that you're going to move it.
@syranide I'm talking about <video>
not <iframe>
. Moving a video causes it to pause, but not reload.
https://github.com/facebook/react-native/issues/14508
Re-parent is important for react-native.
You probably won't like this solution, especially for the browser, but I've been thinking about using Yoga for my particular use case.
The issue is React can't deal with reparenting. Then fine, I'll put the parent/child tree inside Yoga. I'll ask it to compute where everything will be. Then I'll put all the stuff under a single parent in React with absolute positions and sizes. Not the entire app mind you, just those parts that need to have the same parent in order to solve this issue for me.
We'll see how it goes.
That might suggest other solutions. For example maybe you could tell react a virtual parent
<SomeComponent virutualparent={???} ...
If defined it could use that to find if a node moved? Just thinking out loud.
Has there been any progress on talks about reparenting?
I can throw in my two cents from a couple of use case examples.
We have a declarative UI application built built using Backbone but with an ongoing attempt at converting into React. In the application we have a splittable container of dynamically loaded sub items. This container can be further split to contain more items. So we have a deep structure of splittable containers, containing dynamic items as leaves. Whenever a container is split (an item is added), the item contained in it gets reparented into a new splittable container with the newly added item. This causes the sub item to lose all state. (We might, eg. have a button component in the sub item that holds its own animation state. After reparenting the animation disappears, naturally.)
We have draggable, drag&droppable popups in this UI application. They are originally opened to freely float above the UI Frame but can be moved to be held in a list of popups. This is currently done by reparenting the popup component to the list, but naturally once again the popup item is unmounted and remounted during the reparenting, and all state is lost.
I can imagine at least #2 being solvable through smart usage of ReactDnD and Portals. However, #1 seems to be straight up impossible without eg. generating our own DOM nodes into which the React trees are rendered, and which are then appended into the container els. No amount of Portals sound like being enough, as the DOM element references generated from the containers would be unmounted, causing Portals to be called with null before the containers are remounted and the Portals can be rendered again with the fresh references.
It does feel to me like some sort of reparenting support would be beneficial to React. Although it definitely goes against the base tenets in some ways, I would still argue that it is a far better option than to eg. force all component state into application state or some opaque state object passed down from high enough above to survive the reparenting.
We have exactly the same problem with transitions where a transition container has two animated views that animate previous and next children.
<TransitionContainer>
<AnimatedView>{ oldChildren }</AnimatedView>
<AnimatedView>{ newChildren }</AnimatedView>
</TransitionContainer>
Re-mounting the oldChildren
causes not only slow down but some visual artifacts on components that do network requests or load huge sets of data.
@pronebird Here's what I wrote up based on this blog post: Reparentable.jsx I'm not sure if this will help in solving your problem, but I'll explain the gist of it.
So the Reparentable component is a true React component that, inside it, manually creates a DOM node to which it ReactDOM.render's any children it has been given. (Children must thus be a single React Component, use React.Fragment if needed.) Additionally, Reparentable requires a ref to which attach the manually created DOM node.
What is :awsm: here is that Reparentable tries to implement a React createElement -like call API for controlling the manually created DOM node. So when you want to create a reparentable element, you could use Reparentable as an alias like createReparentableElement
and then use it just as you'd use createElement
. I tried a little too hard to make this work smoothly, but now looking at it I'm not sure it actually does work (Note, we've not used this _extensively_.)
Caveats:
<<"div", {}, children>>
to Reparentable("div", {}, children)
calls.I've opened an RFC with a proposal for an API that allows for reparenting:
Isn't there a hack where you can store a vDOM tree in a variable and just add it to different parents?
const cached = <div>test</div>
...
render = () =>
<div>{cached}</div>
@esseswann All you would be caching in that example is the React "element" (a wrapper object that contains the type, props, ref, etc). Elements are templates that are used to create component instances or DOM nodes. If you reparent an element, React throws away and recreates the component or node.
This is a nice overview of elements, components, and nodes:
https://medium.com/@dan_abramov/react-components-elements-and-instances-90800811f8ca
Oh, you're surely right, my head was a little in the clouds because I was thinking of another performance issue when I stumbled upon this thread: at some point when you have a whole lot of elements the very object generation for vDOM tree becomes expensive, it's noticeable when you're deleting a single element from a huge collection. Interestingly in this case the DOM operation is extremely cheap compared to reconciliation.
I have even created a helper component for maps which allows to reuse previously created ReactElements by mutating a special local variable directly and then calling rerender when onRemove
callback is called.
Do you have plans on researching how to lower performance impact in such scenarios?
I have even created a helper component for maps which allows to reuse previously created ReactElements by mutating a special local variable directly and then calling rerender when onRemove callback is called.
Not _quite_ sure what you're describing, but I'd be careful of mutation like that with the upcoming async mode (or even with current error boundaries). š
Do you have plans on researching how to lower performance impact in such scenarios?
I'm a little unsure of what scenario you're describing (and I don't want to hijack this GH issue thread). Maybe we could chat somewhere else?
Another possible solution for reparenting is allow to invalidate some of the react tree and force react reconciliation to start again. This will allow to make DOM modifications by external libraries like sortable, dnd, animations etc and then force React reconciliation to start over. I did not find any way to do this, but this possibility will be awesome.
I am getting into this issue. I suppose flutter is using GlobalKey to solve this issue. That is a cheap solution that we can adopt it here.
Widgets that have global keys reparent their subtrees when they are moved from one location in the tree to another location in the tree. In order to reparent its subtree, a widget must arrive at its new location in the tree in the same animation frame in which it was removed from its old location in the tree.
@jakearchibald I am trying to solve the exact same pop-out video issue. Did you ever find a solution that was suitable?
To @syranide's suggestion:
I'd just like to point out that you can do reparenting manually today, if you render the component you want to reparent into a separate non-React node, this node can then be reparented manually anywhere you like (but remember that you may only put it in empty React nodes).
This is great and I didn't know - just tested and it works as you described!
However it doesn't solve the larger problem of being able to reparent React elements. The thing I want to move is of greater complexity than the parent.
Would this also allow āpausingā a sub tree? Iād use it to reparent into a null container of sorts to turn off that entire tree and then bring it back at some point later. May be a separate issue, if somewhat related.
Use case is having our team build react apps they can plug in to a bigger framework. Weād like to preserve the entire UI state and pause and resume them when they arenāt in use.
Would this also allow āpausingā a sub tree? Iād use it to reparent into a null container of sorts to turn off that entire tree and then bring it back at some point later. May be a separate issue, if somewhat related.
Depends on the implementation, but reactjs/rfcs#34 does support that.
It turns out you can solve reparenting pretty neatly with portals.
I've built a library to do that, based on some of the discussion here and a couple of other issues (#13044, #12247). It lets you define some content in one place, and render & mount it there once, then place it elsewhere and move it later, all without remounting or 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 been working very nicely for me!
Super tiny, zero dependencies: https://github.com/httptoolkit/react-reverse-portal. Let me know if it works for you :+1:
React-reverse-portal looks very cool.
Personally, I don't particularly like the portals approach. So I developed a package to handle reparenting in my App, and I published it under the name of react-reparenting.
The concept is really simple. Once you've set it up, you can just re-render the components.
The transferred Child _(key="2")_:
The approach should be renderer independent (I have not yet had the opportunity to test React Native).
Most helpful comment
I've opened an RFC with a proposal for an API that allows for reparenting:
https://github.com/reactjs/rfcs/pull/34