How best to use with React.StrictMode?

I’m not an expert with ReactJS. I am trying to figure out the best approach for integrating ReactJS with BabylonJS when using React.StrictMode.

I do not want to use react-babylonjs for declarative syntax (yet), but I suspect it would have same problem with React.StrictMode. Same for babylonjs-hook. The React.StrictMode is not addressed in the related Babylon.js docs.

The core problem is that React.StrictMode calls useEffect (and useState) twice while in development. There is no obvious way (to me) to initialize babylon.js only once within a function component.

Below shows the code I’ve used to try and allow everything to be initialized twice (including the engine), but that has some issues and seems very inefficient, even if happens only in development.

Is there some other, better, way to do this? Perhaps I should initialize babylonjs in an external module outside of any reactjs component?

Fwiw, here’s code I used to allow double initialization. Very similar to the useEffect hook in the babylonjs docs, but with unmount handling. It works except for an issue/bug with skybox seemingly not being fully disposed and/or webgl state. But again, means reinitializing babylonjs twice on every mount while in development.

function App() {
  const canvasRef = React.createRef<HTMLCanvasElement>()

  useEffect(() => {
    console.log('useEffect')

    // Create engine and a scene
    const babylonEngine = new Engine(canvasRef.current, true)
    const scene = new Scene(babylonEngine)

    // Create a camera
    const camera = new FlyCamera('Camera', new Vector3(0, 0, -2), scene)
    camera.attachControl(true)

    // Create a default environment
    const envHelper = new EnvironmentHelper(
      {
        createSkybox: true, // When true, there are WebGL errors on 2nd init. No errors if false.
        skyboxSize: 30,
        groundColor: new Color3(0.5, 0.5, 0.5)
      },
      scene
    )

    // Setup resize handler
    const onResize = () => {
      babylonEngine.resize(true)
    }
    if (window) {
      window.addEventListener('resize', onResize)
    }

    babylonEngine.runRenderLoop(() => {
      scene?.render()
    })

    return () => {
      console.log('unmount')
      if (window) {
        window.removeEventListener('resize', onResize)
      }
      envHelper.dispose() // doesn't seem to make a difference
      scene.dispose()
      babylonEngine.dispose()
    }

  }, []) // empty array to run useEffect only once ...but doesn't work for React.StrictMode

// ... etc

Here is the React.StrictMode setup, which I believe is the default in many starter reactjs templates now:

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
)

For now, I moved the Babylon.js init to it’s own class/module, and I manually check for “development” mode to make sure I only init/dispose once when StrictMode is in effect.

Works, but I am definitely interested in better ideas

Repo:

Code snippets:


  // Check if we are in "development" mode.
  // This comes from vite.
  // Webpack and any node dev tools will have something similar.
  let strictModeBypass = import.meta.env.DEV  

  useEffect(() => {
    if (strictModeBypass && canvasRef.current) {
      babylonApp = new BabylonApp(canvasRef.current)
    }

    // Handle unmount
    // First time is result of StrictMode.
    // This handling is in place to support HMR with vite/webpack.
    return () => {
      if (strictModeBypass) {
        strictModeBypass = false
        return
      } else {
        babylonApp?.dispose()
        strictModeBypass = true;
      }
    }
1 Like

I wonder if @brianzinn might know?

I use StrictMode and it doesn’t bother me that things were running through twice - since it won’t happen in production. The first run will unmount and tear down properly. I guess I don’t see any issues, so if you can point out what the problem is?

You can turn off StrictMode by removing the component from your index.tsx/index.jsx, but it’s designed to help you find issues in your code, which is why it’s added by default to CRA projects.

One is was in a scene that was loading larger models with significant pre-processsing. The double init after every minor code change (due to HMR) was slightly annoying. That is minor though, and probably a non-issue, as you said. The StrictMode checking can easily be removed.

The second issue is maybe a minor bug in Babylon.js. When using EnvironmentHelper to create a skybox, it seemed impossible to dispose/reconstruct when using StrictMode, without getting WebGL errors.

I tried to create a PG to show the issue just now, and at first I was seeing exact same errors that had been seeing locally consistently.

PG:

Example errors in console:

  • engine.dynamicTexture.ts:93 WebGL: INVALID_OPERATION: texImage2D: no texture bound to target
    or
  • WebGL: INVALID_VALUE: getProgramParamter: attempt to use a deleted object

…but it random as to which errors were received.

Right before I was about to post here the errors mostly stopped. Now, I’m only seeing it randomly if I click the button repeatedly, so I am assuming it’s some sort of timing related issue. Testing this in the PG isn’t straightforward. And again, this is minor and can be worked around. I was mostly curious to see if there was some better way of handling.

Agreed, the StrictMode is useful and should be used whenever possible.