RecoilJS RecoilRoot not accessible within ThreeJS Canvas - javascript

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.

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;

Related

Pass data from Child to Parent in React Three Fiber onCollide function

I need to pass the event inside onCollide function to the parent component. I have tried with props.handleCollide(e); but I got an error: props.handleCollide is not a function. If it helps, I'm using React Three Fiber, a React library for Three.js.
Sphere.js
const Sphere = (props) => {
const [ref, api] = useSphere((index) => ({
mass: 1,
onCollide: (e) => {
// I need to pass the event 'e' to Parent Component
props.handleCollide(e);
},
position: props.position,
...props,
}));
return (
<mesh castShadow position={props.position} ref={ref}>
<sphereGeometry args={props.args} />
<meshStandardMaterial color={props.color} />
</mesh>
);
}
export default Sphere;
Another attempt was to do props.event = e but I got other error: Cannot add property event, object is not extensible.
The main goal is to do something like this:
<Sphere position={[0, 3, 0]} args={[0.5]} color="purple" handleCollide={handleCollide} />
And then catch the event and do the logic.

How to set a ref when using React.cloneElement on a Function component?

I'm writing a Tooltip component and came across the following problem -
Function components cannot be given refs
My code looks like this
Component that includes the Tooltip:
<Tooltip title="Title" description="Description">
<Selection value={copy.value} label={copy.label} />
</Tooltip>
Tooltip component:
import React, { useRef } from 'react'
const Tooltip = props => {
const myRef = useRef()
const child = React.cloneElement(props.children, {
onMouseEnter: () => { /* placeholder */ },
onMouseLeave: () => { /* placeholder */ },
ref: myRef,
})
const TooltipOverlay = () => {
// will be a div wrapped in react Portal
return null
}
return (
<>
{child}
<TooltipOverlay />
</>
)
}
export default Tooltip
My goal is to not have a visible wrapper in the DOM like div/span or similar.
I need the ref to be able to relatively position TooltipOverlay in the Tooltip component.
All solutions are welcome as long as long as goals above are fulfilled.
I saw this done in rsuite library, not sure how.
Had the same issue and couldn't find a way to do what you wanted.
As a workaround and I guess the usual way is to let the user of the Tooltip pass the ref down and then you can intercept it with forwardRef.
const Tooltip = forwardRef((props, myRef) => {
// now you can clone without a ref and have access to the ref.
})
The user of the tooltip needs to pass a ref however:
const childRef = useRef()
<Tooltip ref={childRef}>
<div ref={childRef} />
</Tooltip>

Can't pass useState() 'set' function to grand child

I'm having issues trying to get my useState variable to work. I create the state in my grandparent then pass it into my parent. Here's a simplified version of my code:
export function Grandparent(){
return(
<div>
const [selectedID, setSelectedID] = useState("0")
<Parent setSelectedID2={setSelectedID} .../> //(elipses just mean that I'm passing other params too)
<div />
)}
Parent:
const Parent = ({setSelectedID2 ...}) => {
return(
<div>
{setSelectedID2("5")} //works
<Child setSelectedID3={setSelectedID2} />
</div>
)
}
From the parent I can use 'setSelectedID2' like a function and can change the state. However, when I try to use it in the child component below I get an error stating 'setSelectedID3' is not a function. I'm pretty new to react so I'm not sure if I'm completely missing something. Why can I use the 'set' function in parent but not child when they're getting passed the same way?
Child:
const Child = ({setSelectedID3 ...}) => {
return(
<div >
{setSelectedID3("10")} //results in error
</div>
);
};
In React you make your calculations within the components/functions (it's the js part) and then what you return from them is JSX (it's the html part).
export function Grandparent(){
const [selectedID, setSelectedID] = useState("0");
return(
<div>
<Parent setSelectedID2={setSelectedID} .../> //(elipses just mean that I'm passing other params too)
<div />
)}
You can also use (but not define!) some js variables in JSX, as long as they are "renderable" by JSX (they are not Objects - look for React console warnings).
That's your React.101 :)
Here's a working example with everything you have listed here. Props are passed and the function is called in each.
You don't need to name your props 1,2,3.., they are scoped to the function so it's fine if they are the same.
I moved useState and function calls above the return statement, because that's where that logic should go in a component. The jsx is only used for logic dealing with your display/output.
https://codesandbox.io/s/stupefied-tree-uiqw5?file=/src/App.js
Also, I created a working example with a onClick since that's what you will be doing.
https://codesandbox.io/s/compassionate-violet-dt897?file=/src/App.js
import React, { useState } from "react";
export default function App() {
return <Grandparent />;
}
const Grandparent = () => {
const [selectedID, setSelectedID] = useState("0");
return (
<div>
{selectedID}
<Parent setSelectedID={setSelectedID} selectedID={selectedID} />
</div>
);
};
const Parent = ({ selectedID, setSelectedID }) => {
setSelectedID("5");
return (
<div>
{selectedID}
<Child setSelectedID={setSelectedID} selectedID={selectedID} />
</div>
);
};
const Child = ({ selectedID, setSelectedID }) => {
setSelectedID("10");
return <div>{selectedID}</div>;
};
output
10
10
10
const [selectedID, setSelectedID] = useState("0")
should be outside return

setState in separated component

So I'm in the middle of my project and I found a problem that's too complicated for me.
I need to use sth like this:
{this.state.isOpen && <MyComponent />}
but state that I want to refer to is inside hoc, let's name it AppHOC. AppHOC works with some components that I need to display in Root by clicking on an but Icon is also separated and it looks like:
const Root = () => {
return(
{this.state.isOpen && <MyComponent />}
<Wrapper>
<Icon />
</Wrapper>
);
}
So the problem is: App onClick have to setState of AppHOC and recieve one of functions that AppHOC contains, and Root file have to get that state from AppHOC. Is it possible? Can I do it using Redux? Does Redux work with HOC like one reducer but state separated for every generated MyComponent? Should I use HOC here or not?
This is a common case for lifting the state up. A common parent should host the state, which seems to be Root in this case.
Since passing a state through props to
multiple deeply nested components can be cumbersome, it can be passed through React context:
const OpenContext = React.createContext();
const Root = () => {
let [open, setOpen] = React.useState(false);
let openState = React.useMemo(() => [open, setOpen], [open]);
return(
<OpenContext.Provider value={openState}>
{open && <MyComponent />}
<Wrapper>
<Icon />
</Wrapper>
</OpenContext.Provider>
);
}
The state can be accessed in nested components with context API:
let [open, setOpen] = React.useContext(OpenContext);
This is a problem that Redux can solve but it's not required.

Passing refs in React

In my App component, I have 2 components Navbar and View. In my Navbar component, I have an ExportButton component which onClick should generate a screenshot of the View component by passing its ref.
App.js
function App() {
const view = useRef();
return (
<div className="App">
<Navbar takeSnap={view}/>
<View ref={view}/>
</div>
);
}
Navbar.js
const Navbar = ({ takeSnap }) => {
return (
<>
<Lists />
<ExportButton takeSnap={takeSnap} />
</>
);
};
Button.js
const ExportButton = ({ takeSnap }) => {
function handleClick(takeSnap) {
domtoimage.toBlob(takeSnap.current, {}).then(function (blob) {
saveAs(blob, "myImage.png");
});
}
return (
<Button onClick={() => handleClick(takeSnap)} />
);
};
I having some trouble passing ref of View to use the library dom-to-image to take a screenshot. The error says "Uncaught (in promise) TypeError: Cannot read property 'cloneNode' of undefined
at makeNodeCopy". This might be a quick fix but I'm not sure where I'm going wrong.
You cannot create a ref for a component, a ref can only reference a DOM element.
When you do:
<View ref={view}/>
ref is a reserved keyword and it won't be passed down to your View render function.
You can use forwardRef to solve this problem, or simply use a different keyword such as myRef:
<View myRef={view}/>
Then when you render your View, you can assign this ref to the element you want the screenshot from:
<div ref={myRef} ...

Categories