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.

So is there a resolution for ScrictMode… Use it ? Disable It ?

For well-made Web3D applications, it is advisable to use StrictMode.

In React StrictMode, calling the same function twice serves the purpose of testing whether the object functions correctly despite this.
This particularly tests the engine and the scene’s disposal when used with Babylon.js.

in the above discussion, it was mentioned that using StrictMode causes double initialization, resulting in overly heavy logic being executed twice.

Generally, since scene initialization is performed as an asynchronous operation(due to network requests, etc.), this issue does not arise.
Because if there is an asynchronous operation during initialization, the execution context is yielded to React at that point, and StrictMode causes the first scene to be disposed.

However, for all asynchronous operations, one must always verify whether the scene has been disposed of after the await.
This is because the scene may be disposed of at any time, and the scene’s disposal state may differ between the start and completion of the asynchronous operation.

Below is the actual code I use to handle this.

const animation = await this._animationLoader.load(fileName);
if (animation === null) return false;
// After await, you must always check the scene's dispose state.
if (this.scene.isDisposed) return false; 

Indeed, considering the disposal of a scene after asynchronous operations is only natural, and StrictMode can be seen as aiding in pre-testing such disposal logic.

1 Like

When I originally posed this question (a few years ago), I was transitioning from vue to react. I’ve used React extensively since then. Recently, I’ve been seriously using Babylon.js again.

I generally agree with @noname0310’s summary. StrictMode should be used in almost all cases. However, I think there are some caveats.

First, consider that there are really two different topics when StrictMode is in effect:

  • A) Testing that useEffect is being used correctly in the React context.
  • B) Testing that back-to-back initialization and disposal of BJS engine and scene(s) is working correctly.

In the case of (A), there is never a good reason that StrictMode should be worked around. However, with (B), I think it’s less clear. Consider that when testing back-to-back init/dispose of BJS, there are multiple layers being tested:

  • User code →
    • Babylon.js →
      • Browser (WebGL/WebGPU layers) →
        • OS Layer →
          • Video Driver →
            • Hardware →

Most of these layers are opaque, and some of those layers are doing asynchronous operations, including at the hardware layer. I point this out to note that doing back-to-back init/dispose of this entire stack requires correct and complicated handling at all layers, and there have been numerous bugs related to this at all layers. In real world apps, it’s uncommon to do immediate back-to-back init/dispose, so issues related to this aren’t often caught. The use of StrictMode with Babylon.js means those issues will surface though.

As one example, I updated my “vite/react/babylon.js” starter repo:

It shows how I handle BJS init/dispose with React StrictMode. And how I dynamically create the canvas to try avoid some issues. However, even with this very simple scene and setup, if I don’t set BJS’s non-obvious engine init parameter loseContextOnDispose to true, then I will get multiple warnings in browser console on the second init, like this:

WebGL: INVALID_OPERATION: texImage2D: no texture bound to target

I don’t know why. I would say 9 times out of 10, when I see errors or warnings like this, it’s because I (or one of the BJS scene helpers) have hanging references and/or I didn’t dispose things correctly. But in this case? I suspect something related to the BJS env helper, but I haven’t yet debugged. It’s still possible I have an error somewhere and I would be grateful if someone pointed it out. Otherwise, this is an example of where StrictMode can cause problems.

Other cases I’ve been dealing with recently where StrictMode has been problematic:

  • I think when using instanced meshes, if things are not disposed correctly, the 2nd init causes initialized meshes not to show. No errors or warnings. I didn’t debug this 100% so again, maybe was my own error somewhere. But for this reason, I’d suggest that when dealing with unexplained issues in BJS, it can be useful to remove StrictMode temporarily to see if that resolves things.
  • When dealing with very large 3D scenes, the double init can be cumbersome. …and at the moment, I’m trying to debug a problem where dispose is taking an inordinate amount of time. I suspect it is my own error somewhere, but is proving very hard to find.

As one last note: When in a situation where you don’t want, or don’t need, StrictMode testing of your BJS code (but do want to keep StrictMode for other react code), consider simply moving the babylon engine init and code to a global variable in your module. i.e. Forget about useEffect, useRef, etc. You have to then decide when/how to dispose of it when your “page” is compete, but sometimes that doesn’t matter, and that removes it completely from the react render cycle.

1 Like