-
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 reparenting #3965
Comments
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 |
@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 #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. |
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
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 |
@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 |
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 This movement shouldn't impact playback. The generated keys idea in the gist would work I think. As would some kind of |
@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 |
facebook/react-native#14508 |
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
If defined it could use that to find if a node moved? Just thinking out loud. |
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. |
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). 😄
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.
|
@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:
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. |
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 👍 |
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). |
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 |
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. |
Thanks @dantman: i got the same issue and got resolve by reading this. |
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
andblockB
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 thisrender()
is the page root.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, whenlayoutA
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 likeReact.addons.createFragment
might work.The text was updated successfully, but these errors were encountered: