RecoilJS RecoilRoot not accessible within ThreeJS Canvas

500 Views Asked by At

Hoping someone can help me out here. Im quite new to RecoilJS so if Im missing something obvious, please let me know.

I am trying to manage the state of 3D objects in a scene with RecoilJS Atoms. I have an atom for the last item the mouse hovered over and I want to show a debug panel with its info. For some reason the RecoilRoot provider doesn't seem to be accessible from within the ThreeJS canvas.

In Viewer (code below), I get an error warning me that This component must be used inside a <RecoilRoot> component when I try to declare const [hoveredLED, setHoveredLEDAtom] = useRecoilState(hoveredLEDAtom); (full trace below)

However, passing setHoveredLEDAtom down from the parent (Viewer) works.

Declaring it within Debug also works, which is a sibling of Canvas sharing the same contexts

This is fine for now, but the whole point of moving to Recoil was to stop passing props up and down.

Am I missing something obvious or does the ThreeJS canvas somehow exist in a different scope.

Any pointers would be appreciated.

index.js


const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  // <React.StrictMode>
  <RecoilRoot>
    <App />
  </RecoilRoot>
  // </React.StrictMode>

App.js

function App() {
  return (
    <div className="App">
      <Viewer />
    </div>
  );
}

Viewer

const LED = ({ led }) => {
  const [hovered, setHoevered] = useState(false);
  const [hoveredLED, setHoveredLEDAtom] = useRecoilState(hoveredLEDAtom);

  const handleHoverEnter = () => {
    setHoveredLEDAtom(led);
    setHoevered(true);
  };
  const handleHoverExit = () => {
    setHoevered(false);
  };

  return (
    <mesh
      onPointerOver={(event) => handleHoverEnter()}
      onPointerOut={(event) => handleHoverExit()}
    >
      <coneGeometry />
      <meshStandardMaterial
        color={hovered || led.brightness > 125 ? "hotpink" : "grey"}
      />
    </mesh>
  );
};
const Debug = () => {
  const [hoveredLED, setHoveredLEDAtom] = useRecoilState(hoveredLEDAtom);
  return (
    <>
      <div style={{ position: "absolute", left: "10px", top: "1rem" }}>
        member : {hoveredLED.member}
      </div>
    </>
  );
};
const Viewer = () => {
  const [model, setModel] = useRecoilState(modelAtom);
  const [hoveredLED, setHoveredLEDAtom] = useRecoilState(hoveredLEDAtom);

  useEffect(() => {
    console.log(hoveredLED);
  }, [hoveredLED]);

  return (
    <>
      <Debug />
      <Canvas camera={{ position: [5, 7, 5] }} style={{ height: "700px" }}>
        <Helpers />
        <OrbitControls />
        {model.map((led, index) => {
          const key = `led-${index}`;
          return (
            <LED key={key} led={led} />
          );
        })}
      </Canvas>
    </>
  );
};

export default Viewer;

Error

995 react-reconciler.development.js:9747 The above error occurred in the <LED> component:

    at LED (http://localhost:3000/static/js/bundle.js:205:5)
    at Suspense
    at ErrorBoundary (http://localhost:3000/static/js/bundle.js:1998:5)
    at Provider (http://localhost:3000/static/js/bundle.js:3860:5)

React will try to recreate this component tree from scratch using the error boundary you provided, ErrorBoundary.
logCapturedError @ react-reconciler.development.js:9747
2 react-three-fiber.esm.js:141 Uncaught Error: This component must be used inside a <RecoilRoot> component.
    at err (recoil.js:16:1)
    at Object.notInAContext (recoil.js:4092:1)
    at updateRetainCount (recoil.js:3255:1)
    at useRetain_ACTUAL (recoil.js:4669:1)
    at useRetain (recoil.js:4627:1)
    at useRecoilValueLoadable (recoil.js:5234:1)
    at useRecoilValue (recoil.js:5258:1)
    at useRecoilState (recoil.js:5306:1)
    at LED (Viewer.js:76:1)
    at renderWithHooks (react-reconciler.development.js:7363:1)
react-dom.development.js:18525 The above error occurred in the <ForwardRef(Canvas)> component:

    at Canvas (http://localhost:3000/static/js/bundle.js:4179:5)
    at Viewer (http://localhost:3000/static/js/bundle.js:325:83)
    at div
    at App
    at RecoilRoot_INTERNAL (http://localhost:3000/static/js/bundle.js:91093:5)
    at RecoilRoot (http://localhost:3000/static/js/bundle.js:91259:5)

Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.
logCapturedError @ react-dom.development.js:18525
react-dom.development.js:26740 Uncaught Error: This component must be used inside a <RecoilRoot> component.
    at err (recoil.js:16:1)
    at Object.notInAContext (recoil.js:4092:1)
    at updateRetainCount (recoil.js:3255:1)
    at useRetain_ACTUAL (recoil.js:4669:1)
    at useRetain (recoil.js:4627:1)
    at useRecoilValueLoadable (recoil.js:5234:1)
    at useRecoilValue (recoil.js:5258:1)
    at useRecoilState (recoil.js:5306:1)
    at LED (Viewer.js:76:1)
    at renderWithHooks (react-reconciler.development.js:7363:1)
three.module.js:26599 THREE.WebGLRenderer: Context Lost.
1

There are 1 best solutions below

0
On BEST ANSWER

From : https://recoiljs.org/docs/api-reference/core/useRecoilBridgeAcrossReactRoots/

If a nested React root is created with ReactDOM.render(), or a nested custom renderer is used, React will not propagate context state to the child root. This hook is useful if you would like to "bridge" and share Recoil state with a nested React root. The hook returns a React component which you can use instead of in your nested React root to share the same consistent Recoil store state. As with any state sharing across React roots, changes may not be perfectly synchronized in all cases.

So bridging the contexts is a simple as creating a bridge and nesting it within the ThreeJS Canvas

const Viewer = () => {
  const RecoilBridge = useRecoilBridgeAcrossReactRoots_UNSTABLE();
  ...

  return (
    <>
      <Canvas>
        <RecoilBridge>
            ...
        </RecoilBridge>
      </Canvas>
    </>
  );
};

export default Viewer;