-
Notifications
You must be signed in to change notification settings - Fork 558
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
Reparenting API #34
base: main
Are you sure you want to change the base?
Reparenting API #34
Conversation
Does this RFC suggest that React would move around the actual DOM nodes? Is it technically possible? For example, if one of the child node was a playing |
@streamich Yes, DOM nodes would be detached with removeChild if the reparent tree is not rendered, and re-inserted later with appendChild/insertChild when added back. If one of the children of the reparent is a video element this does imply that the video element inserted in the other parent would be the exact same video element. I cannot make guarantees however about the video still playing. Even if you keep the same instance of the dom node, some DOM elements have quirks when you move them around the dom tree. I'm not proposing any special hacks to handle that. So the video will be the same, but if videos stop playing when you appendChild them into another parent then videos will stop playing when you reparent them. Though on the topic of playing videos. Take a look at my other "registered prop as ref" RFC, one of my examples is a custom |
I think it would reset the |
Well. If you really wanted. You could build a |
@dantman The whole point of reparenting is to keep intact all and any of the states of element that are not controlled by React. Imagine yourself making something like Jupyter. You have a component that wraps CodeMirror, and you'd like to drag and drop rows of the document so that all the state of CM stays the same: selection, focus, scroll etc. There is no way to control all of this state by the component because CM doesn't provide API methods to control every single part of it. In the unlikely case that controlling all props works out with a |
@streamich Yes, it's as simple as |
I guess, for React just re-parenting and leaving it up to the user to deal with consequence is fine as long as it is opt-in. Here is a short list of consequences though:
|
Seems to have some overlap with #7? |
text/0000-reparenting.md
Outdated
|
||
A Reparent is tied to a component, when this component is unmounted the reparent is unmounted and the detached tree is discarded. This allows reparents to hold detached trees without doing so in a permanent way that would leak memory. | ||
|
||
A Reparent has an additional `.unmount()` method, this can be used to force the reparent to unmount and discard its tree like it would when the component it is tied to unmounts. This allows reparents to be hoisted upwards and used for dynamic content and explicitly unmounted when necessary. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this conflicts w/ the previous paragraph. If a reparent can be hoisted upwards, and the component it was tied to is unmounted, then it gets unmounted and discarded. Is there a method for changing the owner of a reparent?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for not being clear. By hoisting for dynamic content, I mean you can move the React.createReparent(this)
higher in the tree (hoisting), this higher node you use this
from is the "component it is tied to"/owner, you can pass the reparent down to children using context/props and use it in descendants, then if the dynamic reparent is no longer needed you can call .unmount()
(usually at the higher node) in order to unmount and discard whatever you put inside the reparent tree.
Perhaps I should define a glossary of things like 'state tree', 'dom/native tree', 'reparent owner', etc... in the RFC and use those terms to make things clear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, so not at runtime, but during development. Got it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, sorry. I thought it was clear by using "hoisting" because I normally when I hear "hoisting state" it's referring to moving the state = {}
and setState
definition from one Component to a parent component.
@polkovnikov-ph But my intent with mentioning class MyComponent extends Component {
content = React.createReparent(this);
render() {
const {show, children} = this.props;
const content = this.content(<Fragment>{children}</Fragment>);
if ( show ) {
// Show the contents
return content;
} else {
// Don't render the contents, the dom/component tree will not be
// present in the page but it will be retained as a detached tree
// and later re-used when `show` becomes true again.
return null;
}
}
} If you move a dom node from one parent to another in a render such that the reparent is never left out of the react component tree, I do think it would be ideal for React to just use If there are dom states that get discarded when you I can also add a limitations section noting the dom nodes that lose state when you reparent them with appendChild. Would that be sufficient? |
I'm unsure, but it seems that it's possible to create a loop of reparenting components that locally looks as if it's possible to use only |
To dive more into this issue of removeChild/appendChild and state loss I've built a series of simple dom reparent tests that will reparent an element using just appendChild, removeChild + appendChild, and using a delay before re-insertion. Feel free to propose other elements https://dantman.github.io/dom-reparent-tests/ Summary
ConclusionIt would be nice to still note that React should try to use appendChild without first using removeChild if it does not need to. However this actually isn't a big deal. In practice it appears that almost nothing is lost during removeChild that is not lost during appendChild (as long as you re-insert within the same tick). The only difference I could find is that IE11 will pause the video when you removeChild+appendChild but not appendChild. But this is a pretty pointless difference because Chrome pauses the video on even a basic appendChild, so you are going to have to make the playing state a controlled behaviour anyways. Results
|
Also add unresolved question about rendering the children of a detached dom tree.
Something else that is hard to test is selection behaviour. This probably isn't an issue for overall selection (I notice that even when completely removing and creating dom nodes a selection outside of them will update to contain even whole block nodes). However it may be relevant for reparenting large paragraphs with some selected text in them or large textarea inputs with a selection being edited. |
Important discussion points: Is it possible to hydrate a server-rendered DOM tree and know, without needing deterministic IDs, and know what portion of it belongs to a Reparent and should be detached and held on to when unmounting instead of destroyed? (Or would we need to wait for the internals of #32 to solve this) Should the reparent function ( When a Reparent's tree is detached from the DOM (the DOM nodes are not part of the document, but the tree still exists so it can be re-inserted later) and the Reparent is given new children. Should it render those children into the detached tree; or, should it just store the new children and wait until the tree is re-mounted before rendering them. I can see rationales for both:
|
Tested in Chrome, Firefox, Safari, and IE11. All browsers lose focus on basic appendChild. https://dantman.github.io/dom-reparent-tests/focus.html
This demonstrates the usage of dynamic reparent creation and removal, without the complex multi-component handling of the dynamic template example.
@philipp-spiess What I was thinking is that if you stopped rendering the |
The ability to update the contents of a reparent is one of the key pieces of functionality we are going for. But also that proposal would permanently leak memory since it doesn't have a way to dispose of the reparent when you are no longer using it. |
@philipp-spiess I was thinking something along the following lines (though thinking deeply on this I keep finding more and more problems so this isn't going to be a whole lot of good I think): I would have The React.Detached Component would again be similar to React Fragment; inside it you can render any Components and the contents is diffed normally for changes. Yes, I mean you could also render normal divs etc. inside this component. It would act the same as creating a DOM tree manually without appending it to be a child of the body. This would, however, be mostly useless without the Reparent Component, as the moment you "move" your normal DOM elements from within the React.Detached to some actual DOM tree, the Components of course unmount and are destroyed, and new ones created in the new location. The Reparent Component could however use the React.Detched to keep itself alive while detached without ill effects. EDIT: React.Detached could be created in userland more or less as follows: React.Detached = elem => {
const target = document.createElement("div");
return ReactDOM.createPortal(elem, target);
} So the usage of these would be something like: const ReparentA = React.createReparent(); // This would more likely be created inside some parent component during mounting or updating based on some inflowing data
class MyPointOfForking extends React.Component {
render() {
const {
children,
condition,
} = this.props;
return (
<ForkA>
{condition === true && <ReparentA>{children}</ReparentA>}
</ForkA>
<ForkB content={condition === false && <ReparentA>{children}</ReparentA>} />
<React.Detached>
{typeof condition !== "boolean" && <ReparentA>{children}</ReparentA>}
<React.Detached>
); So we have some React tree (children in this case, but could just as well be created inside the render function) that we want to render somewhere, but the location changes based on some condition, or it can be detached from the DOM entirely in some cases. Thus we have two choices by and large: Either we place the ReparentA Component (remember, this is a unique individual) in a normal React DOM tree one way or another (ForkA and ForkB, where ForkA will render directly as children, while ForkB might likely pass the content prop onwards to somewhere even deeper, before it finally gets rendered as a child), OR we can render the component in a detached React DOM tree. Doing more than two of these will cause an error. You cannot render a Reparent in two places, not even if one of those is a Detached DOM tree. I can see some problems with this approach though:
Some good things:
|
The ability to "keep" a reference while still rendering it elsewhere is a requirement. This API won't work well when you have to wait for something async to finish when moving a reparent from one child to another. |
I don't quite understand what kind of reference do you want to keep, and what would you want to do with it. Also, what do you mean by "wait[ing] for something async to finish when moving a reparent from one child to another"? Do you mean that part of the content of a reparent will move first, and some other part will be moved later once some async action has finished? |
By keep a reference I mean it should be possible for a component to use a reparent such that it will not unmount, without providing children, and while still allowing another component to actually render the reparent. By waiting for async, I'm referring to a scenario where a parent with dynamic contents passes reparents to children (likely using context), the children render the reparents, then the dynamic content changes. If the dynamic content changes such that a reparent immediately moves from one child to another child then the reparent would normally move fine. However, if the second child happens to need to wait for say some API call to fetch before it can render then the reparent will not have a destination until that is fulfilled. If the parent cannot essentially say "this reparent is still in use, even if no-one is currently rendering it" then while we are waiting for the API call to finish the reparent will end up unmounting instead of detaching so it can be inserted into the second child when it renders. |
Reference: What does the first component "use" the reparent for if it does not provide content for the reparent, and does not render the reparent? Waiting for async: In my opinion this is simply a question of keeping proper tabs on the reparent components, just as you would keep proper tabs on components you create in normal renderers. eg. The second child needs to wait for some API call before it is ready to actually render the reparent. Then in its renderer it should return If it really is thought necessary to allow for the parent to essentially call |
There will still be problems with Async React. In the future, React might render and commit one part of the tree before the other part renders (see this tweet from Dan). This means that there might be a "hole" when no reparent is rendered although you're immediately moving it into a different subtree. This is why we need an API where a shared controller can tell React to not GC certain reparents - No matter if they appear in a commit phase or not. |
The parent knows that the item the reparent is associated with still exists. It does not know if it will be rendered.
How about the alternative JSX based api? That uses a |
I do not know much anything about Async React, having already forgotten all that was said in the presentation. I believe this is not an abyss we should look too deeply into. Dan mentions avoidance of "tearing" and if reparents were a current part of React, I'm sure reparents losing state / unmounting would be considered "tearing". He also makes a vague reference to the postponing of commission being an opt-in thing, so such a situation where a reparent gets unmounted due to postponed rendering could also be argued to be a user error.
Mmm... I'd want to say that the AsyncElement here should take the reparent from the context and either render it in React.Detached, or pass it as a prop to Child once it is done with the necessary request. Similar to lifting of state and all that. Of course, if AsyncElement here is some 3rd party component that you can't mess around with, you may be in quite some trouble.
... It's okay, maybe. I'm not exactly thrilled with it, as I already managed to sell myself on my idea. (Surprise!) I've managed to convince myself that a as close to a normal element as possible is the prettiest and most elegant solution and thus the best. So having this extra Reference thing is weird as it doesn't really have any precedent and smells a bit of laziness ("I don't know if my stuff is actually needed, so I'll just keep it around just to be safe..."). The whole Reference / keep() -system feels a little like tying the Throwback:
Why? What would an empty reparent accomplish? Except work as a different kind of Rendering the reparent as empty would, of course, work as well but it would simply not do anything nor would it be a necessary thing in order to keep the reparent usable for a future case. I can only see this being a reasonable situation when the only child (or all children) of a reparent return |
From what I've seen of the suspense api so far. It's encapsulated enough that forcing a parent to know enough about what it's children are rendering in order to know when it needs to render a detached node in some apps would break encapsulation. And a reparent unmounting because a placeholder was rendered temporarily is not user error. Additionally while suspense is great, it's not the only way to do async react. Apps are currently already doing async react using componentDidMount and state. Even if suspense were to somehow solve the reparent issue internally and avoid unmounting something that may be used in the tree it is waiting for; it still wouldn't solve the issue for all the apps currently not using suspense for fetching data.
Even if we assume AsyncElement wasn't a 3rd party component, pretend it was say a lightweight component tasked with downloading i18n data and then rendering its children once that data is available. It would be a pretty big encapsulation break to say that knowledge of Child's reparent should be hoisted into AsyncElement. And frankly even the fix of having the Parent pass a "render this if you aren't going to render Child at the moment" down through the tree gets pretty messy as you try to add layers.
My issue was primarily with the distinction between A) React allows you to render an element with no children, and the only difference is the absence of the data in the
It actually feels like the reverse for me. I think of it similar to GCing. React doesn't unmount things as an explicit order from someone to unmount something, it unmounts things implicitly when they are no longer in use anywhere. Just like how a GC tracks what objects are being used and removes any objects that are no longer referenced. Having to explicitly track and Aside from that, people brought up some legitimate concerns about the explicit
I was going to agree and arguer further that the idea of a unique instance const FooContext = React.createContext();
// ...
<FooContext.Provider value='foo'>
<FooContext.Consumer>
{foo => <span>{foo}</span>}
</FooContext.Consumer>
</FooContext.Provider> React's new context system's Provider and Consumer are unique component instances where the component itself is tied to the context of the FooContext. And they also are commonly used in the The only modification I need to make to my suggested API to make it behave exactly like the context API is to make it so that you don't use Reparent directly as an element. const Reparent = React.createReparent();
// Keep a reference to the reparent, without changing its contents.
// Can be used as many times as you want.
<Reparent.Reference />
// Render something into the reparent's context
// and render the reparent into an element.
// Can only be rendered in one location at a time.
<Reparent.Scope>...</Reparent.Scope>
// Render a reparent into a detached state.
// This is distinct from just using `<Reparent.Reference />` to keep a reference
// to avoid the component unmounting and it detaching when it's no longer rendered.
// The distinction is that in this case you are explicitly updating the
// reparent's contents while it is detached.
<React.Detached>
<Reparent.Scope>...</Reparent.Scope>
</React.Detached> All three of these terms are up for better naming suggestions. |
Yes, this is good! Although I'd at least give thought to Overall, though, I think this might well be the True Form of the API 👍 Sidenote: I can't remember too well how |
Interesting idea. But no good, that would result in this. const ReparentA = React.createReparent();
const ReparentB = React.createReparent();
const ReparentC = React.createReparent();
<ReparentC.Reference>
<ReparentB.Reference>
<ReparentA.Reference>
<Foo />
</ReparentA.Reference>
</ReparentB.Reference>
</ReparentC.Reference> Doing this dynamically would be a mess. If your dynamic structure uses a list of dozens of children, then the DevTools tree would be dozens of elements deep for everything. And ironically, unless you make use of an additional |
Yeah, I foresaw this. It would be so nice to look exactly like the Context API but it definitely is better without doing so. |
Sorry for throwing in the confusion about async React. I thought more about it and this is an implementation detail and not a point for deciding about the API. 👍 for the latest JSX API - I guess the Here are some alternative name ideas for the
And here are some ideas for
My favorites so far are |
React.Detached would be included since I brought up the use case of rendering a reparent's children while it is detached. And this functionality is useful in non-dom scenarios as well so it makes sense for Detached to not be a ReactDOM/custom thing. |
I'm not sure if the use case is really as pressing that it requires a new React top level API that is probably expected to work with regular elements as well but that will be completely useless unless a reparent is used (since React will throw away the DOM nodes otherwise anyway). () => (
<React.Detached>
<p>Why would I want to do that?</p>
</React.Detached>
); There might be a reason for updating the detached reparents as well (although I'd say that usually it is OK to only update when it becomes attached again) but I think this can be done within a I believe that the smaller the API surface, the more likely the React team will approve it. |
How're we doing on this RFCS? @dantman Is there anything that can be helped with? |
@dantman What do you think about replacing this PR with a newer one that incorporates all the issues discussed? I think this will make the RFC much more approachable since the discussion here is already very long. I remember it took me quite some time to read through all that and we've since doubled the comments 😐 Let me know if I can help you somehow! |
Sorry for being that person, I have not read all of the comments. However I wanted to include a usecaes: I have a React component that I need to transition from a list of posts that is animating, into a React portal which is outside of the to avoid position: absolute and transform issues, then it will transform the location to a new spot in the page (the top full width) and re-attach itself to the react dom again.
Basically I'm trying to make the image transition effect you get in allot of IOS/Android apps. |
IMO it should just move the element and leave the developer to do whatever they would do. For what it's worth, the behavior on moving a video element is clearly defined in the HTML spec (https://html.spec.whatwg.org/multipage/media.html#media-playback):
Note that the element is only paused if it was completely removed from the DOM (i.e. not moved). However in my tests in Firefox and Chrome I found that Firefox follows the spec (continues playing the video) while Chrome violates the spec (pauses regardless of the destination of the element). Nothing is mentioned about currentTime. I'm filing a bug report for Chromium about this: https://bugs.chromium.org/p/chromium/issues/detail?id=910988 So to summarize:According to the spec, video should continue uninterrupted when moved around the DOM, but at worst, you might need to call |
😆Coming back to this years later, I'm surprised that my proposed API still holds up. I thought I might need to revise it to work with hooks. But it looks like the proposal still works for functional components with hooks. Only perhaps with a user land helper for creating a singular reparent within a component. It's hard to sift through the comments and figure out what revisions I accepted. But I am thinking of reversing some of the ones I might have accepted. There was a lot of discussion about a JSX style API. It seems I accepted and got into this idea because of the similarity to the context render props API. However now that I'll need to sleep on some idea of what a reparenting API that fits in with hooks would look like. |
The most compelling argument for reparenting IMHO is page layout. i.e. The ability to write route components that all use a shared How this fits into concurrent mode should probably be thought about. And what the most user friendly API for this use case would be should be thought of. |
This might be a stupid question (I'm not familiar with the inner workings of React), but couldn't reparenting be solved by introducing a |
So, there is a lot to unpack here. As requested in #182, I'm making a pass to review the things that I've been hesitant to review because a proper review would take a very long time to write. So I'll instead leave some cursory comments. Reparenting is one of the Hard Problems(tm). It basically affects all features at the same time. So the bar for a proposal would have to be extremely high. I think over time we've come to realize that "true reparenting" in all senses of the word usually isn't really needed, and can be a distraction. Here's a few concrete scenarios, and what we tend to use instead:
Now, this doesn't mean that reparenting is completely useless. There might be some use cases I've missed. But in many of these, a combination of the above four approaches together with more traditional approaches like lifting state up, is very likely sufficient. On the other hand, reparenting within the React tree raises so many deep questions that it's hard to justify the complexity. |
Now someone please tell me that's out there already and I don't have to write it myself. |
router5 fits the bill I believe: https://router5.js.org/ I've always been a fan of its routing as state approach over the traditional routing as entry points approach taken by most client side routers. router5's author eloquently explains in his talk introducing router5, that routing as entry points is a pattern that was blindly adapted from server side routing, which is mostly stateless and transactional (i.e. an API request will only ever result in 1 route branch being exercised for the lifetime of the request), while client side routing is inherently stateful and dynamic (i.e. an SPA will need to keep track of its "current" route and often transition between many different routes and nested routes in a single browsing session), which is where much of the impedance mismatch comes from. In more concrete terms, an app built with router5 would just have plain react components subscribing to routing state changes and branching accordingly with plain if/else/switch statements and/or map look-ups (this is the actual "routing" part) at any arbitrary depth in the component tree, instead of relying on special snowflake components (i.e. It really is a shame that development on the project seems to have stalled. The concept itself is really simple though. I've been using a few custom routing utils in my own projects based on the same principles for a while. The entire routing logic fits in less than 100 lines and is super easy to reason about. Lately I've also been experimenting with adding some aspect of relative routing introduced by react-router v6 (IMHO that's one of the only redeeming ideas in there unfortunately, as the rest of it seems to be doubling down on routing as entry points), which so far seems to be a great fit but it will need a bit more battle testing. |
Any progress on this issue?
saving refs so many levels deep sounds like the wrong way to go - I think it would be better to actually have an API for that. |
View formatted RFC
Terminology and naming for the
createReparent
function are up for discussion.