-
Notifications
You must be signed in to change notification settings - Fork 47k
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
Allow Portals to be used for Reparenting #13044
Comments
What do you use reparenting for today? |
We use it at @PSPDFKit to avoid re-creating large subtrees (like a page in a PDF with all its annotations etc.) when we for example change the layout hierarchy. We also have other use cases but most of them can be worked around in userland (like avoiding to call render in our PDF backend again - that could be solved with a custom cache). |
Another option would be to fix #12493 for now and continue to work on reparenting support in React before we remove |
So it's more of a performance optimization to you? Or do you preserve state? |
More of a performance optimization, the necessary state is hoisted. Edit: Well we also preserve state right now. But that can be worked around with a custom cache. |
At my company we have an ongoing Backbone -> React migration being done. Avoiding recreations of Backbone components when change of hierarchy layout happens is relatively easy but some of our component implementations rely on internal state to eg. show animations. In these cases once the migration to React happens we would need Reparenting in order to preserve the internal component state. |
I have a use-case with reusable Vanilla JS widgets, that accept custom content, which can be a React subtree too. Here is a trivial example: https://codesandbox.io/s/8x8o81rz52 Currently the integration is done in renderVanillaUi(ref.current, {
title: "Text content",
renderContent: element =>
ReactDOM.unstable_renderSubtreeIntoContainer(
this,
<div>Content from React</div>,
element
)
});
Was there anyone else trying to integrate non-React ui components with React content? |
Hey @just-boris! Your issue can be solved by storing a reference to the vanilla JS element inside the component state. This way, the component will re-render when the element will become ready and you can access I forked your CodeSandbox to demonstrate this behavior, check it out: https://codesandbox.io/s/2xxw57k82y |
Thank you, @philipp-spiess! I checked this approach, it also works for multiple portals within the same component too, for example when you are rendering a vanilla.js table, that may accept React content in cells: https://codesandbox.io/s/jn13n0mo43 renderVanillaTable(this.ref.current, {
...this.props,
renderItem: (index, item, element) =>
this.setState({
[`portal-${index}`]: {
element,
reactElement: this.props.renderItem(item)
}
})
}); Should I consider this as a sort of official recommendation how to integrate such vanilla/react components together? |
To avoid using unstable/deprecated API, here is an example that might be helpful to anyone looking for a practical approach of re-parenting using portals (CodePen): const appRoot = document.getElementById('app-root');
class Reparentable extends React.Component<{ el: HTMLElement }> {
private readonly ref = React.createRef<HTMLDivElement>();
componentDidMount() {
this.ref.current!.appendChild(this.props.el);
}
render() {
return <div ref={this.ref}/>;
}
}
class Parent extends React.Component {
private readonly childContainer: HTMLElement = document.createElement('div');
state = {
down: false,
};
handleClick = () => {
this.setState(prevState => ({
down: !prevState.down
}));
}
render() {
return (
<div>
<p>Down: {this.state.down + ''}</p>
<button onClick={this.handleClick}>Click</button>
{ReactDOM.createPortal(<Child />, this.childContainer)}
<h2>Root 1</h2>
<div key="1">
{!this.state.down && <Reparentable el={this.childContainer} />}
</div>
<h2>Root 2</h2>
<div key="2">
{this.state.down && <Reparentable el={this.childContainer} />}
</div>
</div>
);
}
}
class Child extends React.Component {
componentDidMount() {
console.log('componentDidMount');
}
componentWillUnmount() {
console.log('componentWillUnmount');
}
render() {
return (
<div>
CHILD
</div>
);
}
}
ReactDOM.render(<Parent />, appRoot); |
@gaearon I have a use-case where I am transitioning a react component from within a blogpost list to outside of the react app to avoid z-index and transform issues, then back into the dom once my transition is finished that pulls the Component up to the top of the page and mounts it into a new route's layout. Cheers! |
Could this feature be used in place of https://github.com/javivelasco/react-tunnels (same concept as https://github.com/developit/preact-slots) ? |
I am looking using this in React Native for moving a video to Full-Screen without pausing or glitches and for drag-and-drop and video in a ScrollView while dragging. |
When working with drag-and-drop this seems to be a real issue, as re-parenting is really prevalent. |
@vkatushenok solution works perfectly and is really cool. Thanks a lot! |
I've built a library to solve this properly - it's effectively a bundled up version of the technique above, with a few extra tricks (e.g. you can specify props both where the element is defined, or where it's used). Uses portals under the hood, and allows for reparenting without rerendering or remounting. 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. Super tiny, zero dependencies: https://github.com/httptoolkit/react-reverse-portal. Let me know if it works for you! |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contribution. |
The issue is still valid. There are a few workarounds posted above, but they have performance issues, because the portals content is being passed via |
How can this be done in a React Native project? |
This comment has been minimized.
This comment has been minimized.
@pimterry I've successfully used In my case I have recursive tree structure, where each leaf node can be split in half, and we have to move prev leaf contents into new leaf, thus changing element position in react tree. |
I built a library to manage reparenting with a different approach, which is not based on portals. You can check it out on Github: React-reparenting. There are some examples on Codesandbox: The package should be independent of the renderer you are using, however I have not yet had the opportunity to test it with React Native. |
@paol-imi I just tested the first example in React Native (Converted the divs to Views with Text) and get the following warning when I press the reparent button: "Warning: The parent has no children". |
When reparenting, the package also tries to send the nodes you are working with (e.g. <div>). You can provide a configuration that works for React Native, or disable the automatic node transfer and implement it yourself. |
If you have problems with the implementation, you can file an issue on Github. |
This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment! |
Bump |
This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment! |
I have a use case: A dashboard with widgets rendered in columns with drag & drop between them (using react-beautiful-dnd). Each column is a Droppable here and the individual widgets are the Draggable children of this Droppable. The contents of each widget are completely dynamic and definitely stateful - each widget is pretty standalone and there can be many different kinds of them, so managing state on a higher level is impractical. For example, they can fetch data to display in charts, they can contain a search bar etc. Reparenting is useful here because you clearly don't want to re-fetch data, or lose you search bar value, when switching columns. Not saying there is absolutely no way around this, but yeah, a native way to reparent probably would be nice? :) |
yeah! I have a huge layout system that contains many tabs, and each tab's content is complex. Searched around, Its probably like Jupyter Notebook Lumino dockPanel, tab using css While using Portal, content losing their state. Mostly it's not a big deal. But still some charts or graphs or else should keep it. Maybe there is a way by expose some slightly lower-level APIs, and keep APIs health through release management. |
I found the need for reparenting when I worked on this search sidebar layout https://your.io/search, the state of the search filters need to be preserved while the DOM had to be reparented to achieve the desired UX. I used https://www.npmjs.com/package/react-reverse-portal. |
tried with react-reverse-portal, reparenting can be easily implemented by
nothing more. root.render(
<TransportProvider>
<App/>
</TransportProvider>
)
// App.tsx
const { swapNode } = useTransports()
swapNode("port-0", "port-1")
<div className={"box-a"}>
<Transport id={"port-0"}>
<FirstRenderTarget/>
</Transport>
</div>
<div className={"box-b"}>
<Transport id={"port-1"}></Transport>
</div> |
Do you want to request a feature or report a bug?
feature
What is the current behavior?
Reparenting is an unsolved issues of React(DOM). So far, it was possible to hack around the missing support for it by relying on unstable API (
unstable_renderSubtreeIntoContainer
) to render and update a subtree inside a different container. It's important to note that this API was using React's diffing algorithm so that, similar toReactDOM.render()
, it is possible to keep components mounted.However this unstable API is going to be deprecated soon and recent features like the introduction of the new context API introduced additional issues.
As an alternative to this unstable API,
ReactDOM.createPortal(children, container)
was introduced. However this API is unsuitable for the reparenting issue since it will always create a new mount point inside thecontainer
instead of applying the diffing when called from a different parent (Check out this CodeSandbox where calling the portal from a different portal will cause the<Leaf />
to have a new uuid). The reason for this is that we want multiple portals to be able to render inside the samecontainer
which makes perfect sense for more common use cases like popovers, etc.Before we're going to remove
unstable_renderSubtreeIntoContainer
, I suggest we find a way to portal into a specific node instead of appending to it so that we can diff its contents instead (or implement a solution for #3965 although that seems to be more complicated), similar tounstable_renderSubtreeIntoContainer
.The text was updated successfully, but these errors were encountered: