brianzinn / react-babylonjs

React for Babylon 3D engine
https://brianzinn.github.io/react-babylonjs/
817 stars 105 forks source link

Pass props to Observable function #99

Closed dennemark closed 3 years ago

dennemark commented 3 years ago

Hi,

I am struggling to pass props to a function called by a scene event. In my function I just want to output a boolean prop:

interact = (event) => { console.log(prop) }

The prop is continously false. However in a useEffect such as the following one, the prop is true or false, depending on the parent.

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

I have tried multiple ways to write the event, but still the prop stayed false: ''

scene.onPointerObservable.add((e)=>interact(e))

I even tried to use useEffect to add and remove the observer, as well as combine it with useState, to write the prop into a state . Also writing the interact function as a useCallback function. Still it did not help.

  useEffect(() => {
    console.log("pickInfo", interactionState)
    let observer = babylonScene?.onPointerObservable.add((e)=>interact(e))
    return(()=>{
      babylonScene?.onPointerObservable.remove(observer as Observer<PointerInfo>)
    }
    )
   });

(bit similar: https://github.com/brianzinn/react-babylonjs/blob/fad7492958fd45ab9deba326c9701491a5155a3c/stories/babylonjs/Integrations/pixi-render.stories.js#L48)

I hope it is possible to understand the issue. I guess I am missing certain knowledge about react and was not able to find a suitable solution. Maybe @brianzinn you have some recommendation?

brianzinn commented 3 years ago

The main thing is to make sure that for the observable callback that no values are being passed in by closure (also that you don't shadow another variable), otherwise they will always be the same value. I think that I would need to see a larger set of code to get the context. Here are the hooks for registering and deregistering observables on the scene render: https://github.com/brianzinn/react-babylonjs/blob/fad7492958fd45ab9deba326c9701491a5155a3c/src/hooks.ts#L41

It looks exactly like what you have there as a code sample :) What's also notable and you would know this, but just to make sure - the useEffect(...[list]) is fired when the list props change from React state, but the observable triggers outside of React state once registered. If you mean the interactionState is always false and it is passed in to your observable then it would remain the same by closure (you would need to add to dependency list and have it remove/add again).

dennemark commented 3 years ago

Thanks for your insight! I tried to reduce the code and finally made it work. It seems it was a matter of keeping the state of the scene correctly. useRef is not available for the scene state and useBabylonScene() is not workin as well. So I had to use the onSceneMount function to set the scene state into a variable. Hope this is correct and I am not missing some memoization issues or whatsoever (memo is bit confusing..).

I had to use the useEffect with scene and interactionState as triggers.

If everything is alright, this issue can be closed.

const App: React.FC<any> = ()=>{
  const [state, setState] = useState(false);
  return(
    <>
    <button onClick={(e)=>{setState(!state)}}>Button</button>
    <DefaultPlayground interactionState={state}></DefaultPlayground>
  </>
  )
}
export default App;

const config: any = configuration;

type Props = {
  interactionState?: boolean;
};

const DefaultPlayground: React.FC<Props> = ({interactionState = false}) => {
  const [scene, setScene] = useState<Nullable<Scene>>(null);

  const mount = (e: any) =>{
    setScene(e.scene)
  }
  useEffect(()=>{

    var observer: any = null;
    console.log(scene)
    if(scene !== null){
      observer = scene.onPointerObservable.add(() => {    
        console.log("pickInfo", interactionState)
      })    
    }
    return(() => {
      if(scene !==null){
        scene.onPointerObservable.remove(observer as Observer<PointerInfo>)
      }
    })
   },[scene, interactionState]);

  return (
    <div style={{ flex: 1, display: 'flex' }}>

    <Engine antialias adaptToDeviceRatio canvasId='babylonJS' >

      <BabylonScene onSceneMount={mount}>

        <freeCamera name='camera1' position={new Vector3(0, 5, -10)} setTarget={[Vector3.Zero()]} />

        <hemisphericLight name='light1' intensity={0.7} direction={Vector3.Up()} />

        <sphere name='sphere1' diameter={2} segments={16} position={new Vector3(0, 1, 0)} />

        <ground name='ground1' width={6} height={6} subdivisions={2} />

      </BabylonScene>

    </Engine>

  </div>
  );
};
brianzinn commented 3 years ago

I think it's just the closure always has the same value. By forcing the observable to be re-added you solved it. You could also use a ref (useRef). I didn't try it, but would suspect this would work:

const observerState = {
  interactionState = false;
}

const DefaultPlayground: React.FC<Props> = ({interactionState = false}) => {
  const scene = useBabylonScene();
  observerState.interactionState = interactionState;
  useEffect(()=>{
    var observer: any = scene.onPointerObservable.add(() => {    
        console.log("pickInfo", observerState.interactionState)
    });

    return(() => {
        scene.onPointerObservable.remove(observer as Observer<PointerInfo>)
    })
   },[]);

  return (...)
}

The reason your hook doesn't work is a LOT more nuanced. It's because your hook isn't inside of the <Scene ..>. So, you need to remove Scene and Engine from your default playground and do:

const App: React.FC<any> = ()=>{
  const [state, setState] = useState(false);
  return(
    <>
    <button onClick={(e)=>{setState(!state)}}>Button</button>
    <Engine ... />
      <Scene .../>
        <DefaultPlayground interactionState={state}></DefaultPlayground>
      </Scene>
    <Engine>
  </>
  )
}

If it's still not clear have a look at the storybook story /Hooks/moe-hooks.stories.js.

dennemark commented 3 years ago

True, that might be a possibility, too. I could imagine it works! Thanks a lot!