facebook / react

The library for web and native user interfaces.
https://react.dev
MIT License
224.77k stars 45.85k forks source link

Support for reparenting #3965

Open dantman opened 9 years ago

dantman commented 9 years ago

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 commented 9 years ago

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

vkurchatkin commented 9 years ago

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>;
    }
}
dantman commented 9 years ago

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

gaearon commented 9 years ago

@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 commented 9 years ago

@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 commented 9 years ago

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

syranide commented 9 years ago

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 commented 8 years ago

@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 commented 7 years ago

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

syranide commented 7 years ago

@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 commented 7 years ago

@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 commented 7 years ago

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 commented 7 years ago

@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 commented 7 years ago

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

syranide commented 7 years ago

@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 commented 7 years ago

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 commented 7 years ago

@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 commented 7 years ago

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

edgesite commented 7 years ago

https://github.com/facebook/react-native/issues/14508 Re-parent is important for react-native.

greggman commented 6 years ago

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.

aapoalas commented 6 years ago

Has there been any progress on talks about reparenting?

I can throw in my two cents from a couple of use case examples.

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

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

pronebird commented 6 years ago

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.

aapoalas commented 6 years ago

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

dantman commented 6 years ago

I've opened an RFC with a proposal for an API that allows for reparenting:

https://github.com/reactjs/rfcs/pull/34

esseswann commented 6 years ago

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>
bvaughn commented 6 years ago

@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

esseswann commented 6 years ago

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 commented 6 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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

benwiley4000 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 4 years ago

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:

paol-imi commented 4 years ago

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

horaciosystem commented 3 years ago

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

dantman commented 3 years ago

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 commented 2 years ago

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 commented 3 months ago

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