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 for reparenting #3965

Open
dantman opened this issue May 27, 2015 · 40 comments
Open

Support for reparenting #3965

dantman opened this issue May 27, 2015 · 40 comments

Comments

@dantman
Copy link
Contributor

dantman commented May 27, 2015

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.

@gaearon
Copy link
Collaborator

gaearon commented May 27, 2015

This is a tricky problem. You might want to share your input here.

@vkurchatkin
Copy link

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>;
    }
}

@sophiebits sophiebits changed the title React recreates large trees instead of moving when they are moved to a different parent within the same component Support for reparenting May 27, 2015
@dantman
Copy link
Contributor Author

dantman commented May 27, 2015

@vkurchatkin I don't see any hint or change in your version of the example.

@gaearon
Copy link
Collaborator

gaearon commented May 27, 2015

@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.

@vkurchatkin
Copy link

@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.

@jimfb
Copy link
Contributor

jimfb commented May 27, 2015

@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 #3653 (comment). 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.

@syranide
Copy link
Contributor

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).

@dantman
Copy link
Contributor Author

dantman commented Sep 27, 2015

@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.

@kof
Copy link

kof commented Aug 10, 2016

Maybe more real life use cases can add some priority to this issue ...

@syranide
Copy link
Contributor

@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?

@kof
Copy link

kof commented Aug 10, 2016

@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

  1. mount it in a hidden container
  2. calc the size
  3. unmount
  4. mount again in the visually visible container

What we need is:

  1. mount in a hidden container
  2. calc the size
  3. reparent to a visually visible container without much overhead

@bvaughn
Copy link
Contributor

bvaughn commented Aug 10, 2016

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).

@syranide
Copy link
Contributor

syranide commented Aug 10, 2016

@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).

@kof
Copy link

kof commented Aug 10, 2016

Yes the caveat is the mounting overhead. Somehow we need to avoid second mount and just transfer the element to a different parent.

@syranide
Copy link
Contributor

@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.

@jakearchibald
Copy link

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.

@syranide
Copy link
Contributor

@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.

@jakearchibald
Copy link

@syranide I'm talking about <video> not <iframe>. Moving a video causes it to pause, but not reload.

@edgesite
Copy link

facebook/react-native#14508
Re-parent is important for react-native.

@greggman
Copy link

greggman commented Aug 1, 2017

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.

@esseswann
Copy link

esseswann commented Jun 27, 2018

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?

@bvaughn
Copy link
Contributor

bvaughn commented Jun 28, 2018

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?

@artyomtrityak
Copy link

artyomtrityak commented Sep 22, 2018

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.

@lijunle
Copy link

lijunle commented Oct 19, 2018

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.

@benwiley4000
Copy link

@jakearchibald I am trying to solve the exact same pop-out video issue. Did you ever find a solution that was suitable?

@benwiley4000
Copy link

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.

@natew
Copy link

natew commented Feb 18, 2019

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.

@dantman
Copy link
Contributor Author

dantman commented Feb 18, 2019

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.

@pimterry
Copy link

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 👍

@paol-imi
Copy link

paol-imi commented Apr 28, 2020

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"):

  • is not unmounted / re-mounted (nor its DOM node).
  • maintains its internal state.

The approach should be renderer independent (I have not yet had the opportunity to test React Native).

@horaciosystem
Copy link

Does using useMemo on blockA and blockB child components from the first example, prevent its rerender?

@dantman
Copy link
Contributor Author

dantman commented Jan 19, 2021

Does using useMemo on blockA and blockB child components from the first example, prevent its rerender?

No. That memoizes the JSX and can have a similar effect to memo. But it doesn't provide any sort of unique ID to the tree and React will not move the tree when it changes parents.

@davidroeca
Copy link

Throwing in another use case for reparenting: deeply nested components that need access to a lower-level context, but will be rendered at a higher level in an app.

In my case, I'm referencing two contexts: a sidebar context (that manages whether a semantic ui sidebar is visible/rendered with lower-level content, and a context below that, which manages form state. Due to the structure of my application, it is not easy to move the form state management above the sidebar context (which could have been another solution here).

The sidebar content needs to be managed at a higher level, because of the required html structure of a pushable sidebar:

<Sidebar.Pushable as={Segment}>
  <Sidebar
    as={Segment}
    visible={visible}
    width='thin'
  >
    {reparentedContent}
  </Sidebar>
  <Sidebar.Pusher dimmed={visible}>
    {/* This node will render a context that must be referenced in the reparentedContent */}
    {theRestOfTheApplication}
  </Sidebar.Pusher>
</Sidebar.Pushable>

Thanks to @pimterry for react-reverse-portal -- this is precisely what I needed!

If anyone sees a better approach for this use case, definitely please let me know.

@HamzaAkbar067
Copy link

Thanks @dantman: i got the same issue and got resolve by reading this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests