React-three-fiber: Destroys context

Created on 25 Mar 2019  路  9Comments  路  Source: pmndrs/react-three-fiber

I'm using redux to connect child elements in my scene.

This one took a few hours to track down. I have a simple component connected to redux, like:

@connect(state => ({}))
class MyFriend extends Component {
  render() {
    return null;
  }
}

If I render it outside a <Canvas>, it works fine:

      <div>
        <MyFriend />
        <Canvas />
      </div>

However if I render it inside a Canvas:

      <div>
        <Canvas>
          <MyFriend />
        </Canvas>
      </div>

Then the app crashes:

Uncaught Invariant Violation: Could not find "store" in the context of "Connect(MyFriend)"

I suspect it's related to the custom rendering here, but I'm still digging into it: https://github.com/drcmda/react-three-fiber/blob/master/src/canvas.js#L197-L204

Most helpful comment

This didn't work either:

Canvas.contextTypes = { store: PropTypes.object };
Canvas.childContextTypes = { store: PropTypes.object };

I'm not sure if this is related to the useEffect() call internally, or the .memo() of the Canvas component?

This works (I think), which should probably give me a clue as to what's going on. It's not ideal:

import { ReactReduxContext, Provider } from 'react-redux';

...

        <ReactReduxContext.Consumer>
          {({ store }) => (
            <Canvas>
              <Provider store={store}>
                 ...
              </Provider>
            </Canvas>
         )}
      </ReactReduxContext.Consumer>

All 9 comments

I tried returning the first argument here but same result so far https://github.com/drcmda/react-three-fiber/blob/master/src/reconciler.js#L237-L242

This didn't work either:

Canvas.contextTypes = { store: PropTypes.object };
Canvas.childContextTypes = { store: PropTypes.object };

I'm not sure if this is related to the useEffect() call internally, or the .memo() of the Canvas component?

This works (I think), which should probably give me a clue as to what's going on. It's not ideal:

import { ReactReduxContext, Provider } from 'react-redux';

...

        <ReactReduxContext.Consumer>
          {({ store }) => (
            <Canvas>
              <Provider store={store}>
                 ...
              </Provider>
            </Canvas>
         )}
      </ReactReduxContext.Consumer>

Ah yes, React is not able to do this, sorry. There have been bug issues about this in all reconcilers i know. Here's one from react-konva in which @gaearon says that currently context chokes at the threshold of a renderer in a renderer.

https://github.com/konvajs/react-konva/issues/188

https://github.com/facebook/react/issues/13336

Your solution, forwarding the context, is the only way right now. I very much wish that the React team will fix this.

Btw. Redux 7 is dropping context again (due to various issues), they'll go back to a subscription model. This would allow connect to cross render boundaries. They have a 7 beta out already. (paging @markerikson in case i got this wrong). That was also the reason we pinned redux at version 5 (we use a reconciler at work and once redux went context all blew up).

@drcmda : we're dropping _propagating state changes_ via context, but we're still using new context to pass down the store itself. That may not go through different reconcilers correctly.

I had some hopes but that makes sense, so it looks like this won't be fixed anytime soon. I think forwarding context is the only solution currently.

I made a helper function that forwards a context to work around this issue:

import { useContext, createElement } from 'react';

export const forwardContext = (Context) => (Component) => ({ children, ...props }) => {
    const value = useContext(Context);
    const wrapped = createElement(Context.Provider, { value }, children);
    return createElement(Component, props, wrapped);
};

And then (note that you use WrappedCanvas instead of Canvas:

import { Canvas } from 'react-three-fiber';
import SomeContext from './somewhere';
import { forwardContext } from './helpers';

const WrappedCanvas = forwardContext(SomeContext)(Canvas);

const App = ({ children }) => (
    <WrappedCanvas >
        {children}
    </WrappedCanvas >
);

@drcmda there is better option described here: https://standard.ai/blog/introducing-standard-view-and-react-three-fiber-context-bridge/

I've used it successfully in my react-three-fiber project.

Could be useful to include it to the library and/or docs

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