React: Allow Portals to be used for Reparenting

Created on 14 Jun 2018  路  27Comments  路  Source: facebook/react

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.

Feature Request

Most helpful comment

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

All 27 comments

What do you use reparenting for today?

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 https://github.com/facebook/react/issues/12493 for now and continue to work on reparenting support in React before we remove unstable_renderSubtreeIntoContainer.

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

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

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

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

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.
An example: https://github.com/atlassian/react-beautiful-dnd/issues/850

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

How can this be done in a React Native project?

Don't delete me

John Ware

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

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.

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

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

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

Was this page helpful?
0 / 5 - 0 ratings