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

Allow Portals to be used for Reparenting #13044

Open
philipp-spiess opened this issue Jun 14, 2018 · 32 comments
Open

Allow Portals to be used for Reparenting #13044

philipp-spiess opened this issue Jun 14, 2018 · 32 comments

Comments

@philipp-spiess
Copy link
Contributor

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 to ReactDOM.render(), it is possible to keep components mounted.

ReactDOM.render(<Foo />, container);
// This won't get <Foo /> to be unmounted and mounted again:
ReactDOM.render(<Foo />, container);

ReactDOM.unstable_renderSubtreeIntoContainer(
  parentComponent,
  <Foo />,
  container
);
// This also won't get <Foo /> to be unmounted and mounted again, no matter if 
// we change parentComponent (and thus call it from a different parent):
ReactDOM.unstable_renderSubtreeIntoContainer(
  parentComponent,
  <Foo />,
  container
);

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 the container 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 same container 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 to unstable_renderSubtreeIntoContainer.

@gaearon
Copy link
Collaborator

gaearon commented Jun 14, 2018

What do you use reparenting for today?

@philipp-spiess
Copy link
Contributor Author

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

@philipp-spiess
Copy link
Contributor Author

Another option would be to fix #12493 for now and continue to work on reparenting support in React before we remove unstable_renderSubtreeIntoContainer.

@gaearon
Copy link
Collaborator

gaearon commented Jun 14, 2018

So it's more of a performance optimization to you? Or do you preserve state?

@philipp-spiess
Copy link
Contributor Author

philipp-spiess commented Jun 14, 2018

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.

@aapoalas
Copy link

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.

@just-boris
Copy link
Contributor

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 componentDidMount method:

renderVanillaUi(ref.current, {
  title: "Text content",
  renderContent: element =>
    ReactDOM.unstable_renderSubtreeIntoContainer(
      this,
      <div>Content from React</div>,
      element
    )
});

createPortal doesn't seem covering this functionality, because createPortal should be called in the render function. However, for widgets use-case the target for portal rendering is not ready at that moment.

Was there anyone else trying to integrate non-React ui components with React content?

@philipp-spiess
Copy link
Contributor Author

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 this.state.element inside the render function.

I forked your CodeSandbox to demonstrate this behavior, check it out: https://codesandbox.io/s/2xxw57k82y

@just-boris
Copy link
Contributor

just-boris commented Jul 19, 2018

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?

@vkatushenok
Copy link

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

@falconmick
Copy link

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

@jgoux
Copy link

jgoux commented Oct 29, 2018

Could this feature be used in place of https://github.com/javivelasco/react-tunnels (same concept as https://github.com/developit/preact-slots) ?
I need to use react-tunnels for my apps layouts, I declare a single Layout component with multiple tunnels/slots in it, and then compose my pages by filling those tunnels/slots. It has the advantage of keeping the Layout mounted and only unmount/remount the slots when transitioning (useful for animations!).

@gastonmorixe
Copy link

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.

@klintmane
Copy link

When working with drag-and-drop this seems to be a real issue, as re-parenting is really prevalent.
An example: atlassian/react-beautiful-dnd#850

@rigobauer
Copy link

@vkatushenok solution works perfectly and is really cool. Thanks a lot!

@pimterry
Copy link

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!

@stale
Copy link

stale bot commented Jan 10, 2020

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.

@stale stale bot added the Resolution: Stale Automatically closed due to inactivity label Jan 10, 2020
@just-boris
Copy link
Contributor

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 setState causing extra render and reconcile loops

@stale stale bot removed the Resolution: Stale Automatically closed due to inactivity label Jan 10, 2020
@dylancom
Copy link

How can this be done in a React Native project?

@warejohn

This comment has been minimized.

@art-in
Copy link

art-in commented Mar 29, 2020

@pimterry I've successfully used react-reverse-portal for reparenting without remounts, thanks.

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.

@paol-imi
Copy link

paol-imi commented May 2, 2020

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:
 - Quick start example.
 - Drag and drop implementation with ReactDnD.

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.

@dylancom
Copy link

dylancom commented May 3, 2020

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

@paol-imi
Copy link

paol-imi commented May 3, 2020

When reparenting, the package also tries to send the nodes you are working with (e.g. <div>).
The default configuration in the package allows you to work with ReactDOM. In your case the warning occurs because no DOM element has been found (since they do not exist in React Native).

You can provide a configuration that works for React Native, or disable the automatic node transfer and implement it yourself.

@dylancom

@paol-imi
Copy link

paol-imi commented May 3, 2020

If you have problems with the implementation, you can file an issue on Github.

@stale
Copy link

stale bot commented Aug 1, 2020

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!

@stale stale bot added the Resolution: Stale Automatically closed due to inactivity label Aug 1, 2020
@dacioromero
Copy link

Bump

@stale stale bot removed the Resolution: Stale Automatically closed due to inactivity label Aug 4, 2020
@stale
Copy link

stale bot commented Dec 25, 2020

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!

@stale stale bot added the Resolution: Stale Automatically closed due to inactivity label Dec 25, 2020
@Brodzko
Copy link

Brodzko commented Feb 5, 2021

What do you use reparenting for today?

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

@aolyang
Copy link

aolyang commented Aug 24, 2023

What do you use reparenting for today?

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 contain: strict and this is the issue.

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.
Luckly, a lib react-reparenting did that by dive into FiberNode with unofficial api. With 2 or more years not update. It's become dangerous if React has breaking change.

reparenting

Maybe there is a way by expose some slightly lower-level APIs, and keep APIs health through release management.

@kuus
Copy link

kuus commented Aug 28, 2023

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.

@aolyang
Copy link

aolyang commented Aug 28, 2023

tried with react-reverse-portal, reparenting can be easily implemented by createPortal.

  1. keep children under a container Transport use createPortal.
  2. use replaceChild to swap content (container) with target container.

nothing more.
made a demo with another different experience try-react-reparent-demo

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>

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