React: Support cross-renderer portals

Created on 6 Aug 2018  路  7Comments  路  Source: facebook/react

Currently createPortal only works within the current renderer.

This means that if you want to embed one renderer into another (e.g. react-art in react-dom), your only option is to do an imperative render in a commit-time hook like componentDidMount or componentDidUpdate of the outer renderer's component. In fact that's exactly how react-art works today.

With this approach, nested renderers like react-art can't read the context of the outer renderers (https://github.com/facebook/react/issues/12796). Similarly, we can't time-slice updates in inner renderers because we only update the inner container at the host renderer's commit time.

At the time we originally discussed portals we wanted to make them work across renderers. So that you could do something like

<div>
  <Portal to={ReactART}>
    <surface>
      <rect />
    </surface>
  </Portal>
</div>

But it's not super clear how this should work because renderers can bundle incompatible Fiber implementations. Whose implementation takes charge?

We'll want to figure something out eventually. For now I'm filing this for future reference.

Reconciler React Core Team Big Picture Feature Request

Most helpful comment

Thanks for working on this! This was a big pain point for us so we made a workaround. In case this is useful, here's an example of the Context barrier (for react-three-fiber) and how we are fixing it temporarily until there's a fix upstream:

CodeSandbox bridge example rotation in context

// 馃憥 Context cannot go through <Canvas>, so <Square> cannot read the rotation
ReactDOM.render(
  <TickProvider>
    <Canvas>
      <Square />
    </Canvas>
    <Consumer>{value => value.toFixed(2)}</Consumer>
  </TickProvider>,
  document.getElementById('outside')
);

// 馃憥 Context is all inside <Canvas> so it cannot be passed/read from outside
ReactDOM.render(
  <>
    <Canvas>
      <TickProvider>
        <Square />
      </TickProvider>
    </Canvas>
    No access to `rotation` here
  </>,
  document.getElementById('inside')
);

// 馃憤 Passes the Context from above, bridging React and react-three-fiber Context
// 馃憥 But this re-renders <Canvas> a lot, partially defeating the point of Context
// 馃憤 memo() Canvas and Square well enough and there's no problem here!
ReactDOM.render(
  <TickProvider>
    <Consumer>
      {value => (
        <Canvas>
          <Provider value={value}>
            <Square />
          </Provider>
        </Canvas>
      )}
    </Consumer>
    <Consumer>{value => value.toFixed(2)}</Consumer>
  </TickProvider>,
  document.getElementById('bridge')
);

鉂わ笍鉂わ笍鉂わ笍

All 7 comments

Thanks for working on this! This was a big pain point for us so we made a workaround. In case this is useful, here's an example of the Context barrier (for react-three-fiber) and how we are fixing it temporarily until there's a fix upstream:

CodeSandbox bridge example rotation in context

// 馃憥 Context cannot go through <Canvas>, so <Square> cannot read the rotation
ReactDOM.render(
  <TickProvider>
    <Canvas>
      <Square />
    </Canvas>
    <Consumer>{value => value.toFixed(2)}</Consumer>
  </TickProvider>,
  document.getElementById('outside')
);

// 馃憥 Context is all inside <Canvas> so it cannot be passed/read from outside
ReactDOM.render(
  <>
    <Canvas>
      <TickProvider>
        <Square />
      </TickProvider>
    </Canvas>
    No access to `rotation` here
  </>,
  document.getElementById('inside')
);

// 馃憤 Passes the Context from above, bridging React and react-three-fiber Context
// 馃憥 But this re-renders <Canvas> a lot, partially defeating the point of Context
// 馃憤 memo() Canvas and Square well enough and there's no problem here!
ReactDOM.render(
  <TickProvider>
    <Consumer>
      {value => (
        <Canvas>
          <Provider value={value}>
            <Square />
          </Provider>
        </Canvas>
      )}
    </Consumer>
    <Consumer>{value => value.toFixed(2)}</Consumer>
  </TickProvider>,
  document.getElementById('bridge')
);

鉂わ笍鉂わ笍鉂わ笍

And just to bring it up once, this also applies to error boundaries and suspense. It would be really helpful if a reconciler could read out contextual information (context, errors, suspense) from its parent reconciler and apply it to itself - i think it shouldn't behave much different than a generic portal.

Well, I hope this gains traction at some point as it's a huge pain point for me right now.

Hi! Any plans on making it come true?

I made something similar to this through a simple implementation that can be shared between renderers.

import React from "react";
import ReactDOM from "react-dom";
import { Portal, createBridge } from "@devsisters/react-pixi";
import { CountContext } from "./countContext";

const $uiRoot = document.getElementById("ui-root");
const uiRoot = ReactDOM.createRoot($uiRoot);
const uiBridge = createBridge(uiRoot, {
  sharedContext: [CountContext],
});

const UIPortal = ({ children }) => {
  return <Portal bridge={uiBridge}>{children}</Portal>;
};

export default UIPortal;

Demo: https://codesandbox.io/s/react-pixi-portal-lsgh1

User can specify the context object to share, and Portal uses React internal readContext() to pass these contexts to another renderer.

I wanted to follow up to say that the solution I proposed above has been working very well for us for 1+ year. The main thing (as I edited in a comment there) is that you have to memo() both the <Canvas> and the <Square> very well, but then everything works very smooth!

We rolled out a useContextBridge hook in the @react-three/drei package for official use within the react-three-fiber ecosystem.

function SceneWrapper() {
  // bridge any number of contexts
  const ContextBridge = useContextBridge(ThemeContext, GreetingContext)
  return (
    <Canvas>
      <ContextBridge>
        <Scene />
      </ContextBridge>
    </Canvas>
  )
}

function Scene() {
  // we can now consume a context within the Canvas
  const theme = React.useContext(ThemeContext)
  const greeting = React.useContext(GreetingContext)
  return (
    //...
  )
}

It's been working out really well and the api is pretty friendly :)

Was this page helpful?
0 / 5 - 0 ratings