React UI + Babylon.js : How to Avoid useState Re-Rendering Canvas?

Hi All,

Brand new to Babylon.js and I feel like I’m missing something obvious here. I’m exploring a proof of concept for a 3D editor focused on physics and robotics and I’ve seen lots of great examples out there of traditional UI’s (assuming react) and the Babylon canvas. @dested has done some great work on QuickGa.me: Web Based 3D Multiplayer Game Engine and Hosting Platform!

When starting to build a React UI and using it to update the state of a mesh, I have no trouble accomplishing this with pure javascript and a state variable outside of my component but when using useState I get a constant re-render every time the state changes. On a certain level, this makes sense, but I’ve seen so many other examples doing this that I’m curious if I’m missing something.

In my examples below, I have a simple slider that is mapping from 0 to 360 and adjusting the rotation of the playground box. With useState my entire Babylon canvas refreshes each time I move the slider. I’ve come across this post How to change mesh properties with react state hook? which creates the same issue.

Any advice on best practices?

Vanilla JS State Handling: Working

import { useEffect, useRef, useState } from "react";
import { ArcRotateCamera, Vector3, Color3, MeshBuilder, HemisphericLight} from "@babylonjs/core";
import { GridMaterial } from "@babylonjs/materials"
import SceneComponent from './SceneComponent';
import SliderComponent from "./SliderComponent";


let box;
let boxRotation = 0;

const BasicPlaygroundComponent = (props) => {

  // Will execute once when scene is ready
  const onSceneReady = (scene) => {

    // This creates and positions an arc rotate camera
    var camera = new ArcRotateCamera("camera1", -Math.PI / 2, Math.PI / 2.5, 75, new Vector3(0, 0, 0));

    // Initialize canvase
    const canvas = scene.getEngine().getRenderingCanvas();

    // Attaches camera canvas
    camera.attachControl(canvas, true);

    // This creates a light, aiming 0,1,0 - to the sky (non-mesh)
    var light = new HemisphericLight("light", new Vector3(0, 1, 0), scene);

    // Default intensity is 1. Let's dim the light a small amount
    light.intensity = 0.7;

    // Our built-in 'box' shape.
    box = MeshBuilder.CreateBox("box", { size: 10 }, scene);

    // Move the box upward 1/2 its height
    box.position.y = 10;

    // Our built-in 'ground' shape.
    var ground = MeshBuilder.CreateGround("ground", { width: 150, height: 150 }, scene);

  };


  //Will run on every frame render.  We are spinning the box on y-axis.
  const onRender = (scene) => {
    box.rotation.y = boxRotation;
  };


  const updateBoxRotation = (val) => {
    boxRotation = (val/100)*Math.PI*2;
  }


  return(
    <div>
      <SceneComponent antialias onSceneReady={onSceneReady} onRender={onRender} id='viewport'></SceneComponent>
      <SliderComponent toUpdate={updateBoxRotation}></SliderComponent>

    </div>
  )
}

export default BasicPlaygroundComponent;

useState State Handling: Re-Renders canvas on each change

import { useEffect, useRef, useState } from "react";
import { ArcRotateCamera, Vector3, Color3, MeshBuilder, HemisphericLight} from "@babylonjs/core";
import { GridMaterial } from "@babylonjs/materials"
import SceneComponent from './SceneComponent';
import SliderComponent from "./SliderComponent";


let box;

const BasicPlaygroundComponentUseState = (props) => {
  const sceneRef = useRef(null);
  const [boxRotation, setBoxRotation] = useState(0)

  // Will execute once when scene is ready
  const onSceneReady = (scene) => {

    sceneRef.current = scene;

    // This creates and positions an arc rotate camera
    var camera = new ArcRotateCamera("camera1", -Math.PI / 2, Math.PI / 2.5, 75, new Vector3(0, 0, 0));

    // Initialize canvase
    const canvas = scene.getEngine().getRenderingCanvas();

    // Attaches camera canvas
    camera.attachControl(canvas, true);

    // This creates a light, aiming 0,1,0 - to the sky (non-mesh)
    var light = new HemisphericLight("light", new Vector3(0, 1, 0), scene);

    // Default intensity is 1. Let's dim the light a small amount
    light.intensity = 0.7;

    // Our built-in 'box' shape.
    box = MeshBuilder.CreateBox("box", { size: 10 }, scene);

    // Move the box upward 1/2 its height
    box.position.y = 10;

    // Our built-in 'ground' shape.
    var ground = MeshBuilder.CreateGround("ground", { width: 150, height: 150 }, scene);

  };


  //Will run on every frame render.  We are spinning the box on y-axis.
  const onRender = (scene) => {
    //box.rotation.y = boxRotation;
  };

  useEffect( () => {
      const scene = sceneRef.current;
      if(scene){
        box.rotation.y = boxRotation;
      }
    }, [boxRotation]);

  return(
    <div>
      <SceneComponent antialias onSceneReady={onSceneReady} onRender={onRender} id='viewport'></SceneComponent>
      <SliderComponent toUpdate={setBoxRotation}></SliderComponent>

    </div>
  )
}

export default BasicPlaygroundComponentUseState;
1 Like

I appreciate the shout out!

I’m not sure about best practices, or common behavior, but for my project I split out the concepts of the UI (initially react, and now solidjs) and babylon completely.

The issue once you do that is communication between the two universes. Originally I was just passing around my setState calls to the babylon side, but I was still causing a lot of rerenders of react which was dragging performance down. Recently, especially after switching to solid, I started using signals more, which is giving me much more surgical access to the html rendering, and much finer grained dom updates.

Your mileage may vary since I really could not waste a single millisecond. The goal is to run on extremely cheap chromebooks with a voxel editor that gobbles up memory, every extra dom update had to be mitigated.

I’m around if you want to chat more about some of the hurdles I’ve gone through building a full web based editor, feel free to DM me, but I will say that I would never have been able to pull it off without BabylonJS :slight_smile:

2 Likes

wrap either the const canvas = scene.getEngine().getRenderingCanvas() or onSceneReady function in a useMemo. You can also wrap the whole function in a memo and remove the props arg. like this
const BasicPlaygroundComponentUseState = memo(() => { stuff }). Remove props because its unused, but if u do use later, changing a prop value would invalidate the memo.

1 Like

in your example SceneComponent will always render with every state change. You could export that component as:

export default React.memo(SceneComponent);

One could avoid using useState at all to go for performance. Those state updates are going to cause renders and scheduling updates. With a ref to the mesh you can alter directly by events and same with DOM to synchronize back and forth. That doesn’t follow the standard React paradigm. With memoization you should get close to same speed.

Also, you shouldn’t notice the “entire Babylon canvas refresh” with each state update - can you post your SceneComponent?

2 Likes

All thanks for the great notes!

OK to summarize, I’ll check out React.memo while also realizing react state may not be the best performance and storing the scene state and meshes in a vanilla JS object outside of my component may be the better way to go. Also looking forward to checking out signals as @dested mentioned.

@brianzinn adding SceneComponent script here. Live example of this entire canvas refresh issue can be seen on BigTex’s react+babylyon.js tutorial here. His demo displaying the issue is live here.

import { useEffect, useRef } from "react";
import { Engine, Scene } from "@babylonjs/core";

export default ({ antialias, engineOptions, adaptToDeviceRatio, sceneOptions, onRender, onSceneReady, ...rest}) => {
  const reactCanvas = useRef(null);

  // set up basic engine and scene
  useEffect(() => {
    const { current: canvas } = reactCanvas;

    if (!canvas) return;

    canvas.width = window.innerWidth
    canvas.height = window.innerHeight

    const engine = new Engine(canvas, antialias, engineOptions, adaptToDeviceRatio);
    const scene = new Scene(engine, sceneOptions);
    if (scene.isReady()) {
      onSceneReady(scene);
    } else {
      scene.onReadyObservable.addOnce((scene) => onSceneReady(scene));
    }

    engine.runRenderLoop(() => {
      if (typeof onRender === "function") onRender(scene);
      scene.render();
    });

    const resize = () => {
      scene.getEngine().resize();
    };

    if (window) {
      window.addEventListener("resize", resize);
    }

    return () => {
      scene.getEngine().dispose();

      if (window) {
        window.removeEventListener("resize", resize);
      }
    };
  }, [antialias, engineOptions, adaptToDeviceRatio, sceneOptions, onRender, onSceneReady]);

  return <canvas ref={reactCanvas} {...rest} />;
};

Looks like that will rerender every time, because your “onRender” method is different every time (see the dependency list). I almost cannot believe my burning eyes and then I look at the console and a new Engine is being created with every button click! :smile: that’s easy to fix at least.

@brianzinn this is interesting, can you clarify? I took the SceneComponent from the Babylon React Docs here. Are you saying the onRender as a dependency at the end of the useEffect is the problem? Or is there another aspect of the code you’re referencing?

every time a render occurs. you create a new function:

const onRender = () => { ... };

Then in your component you are passing in a new version of a dependency of the useEffect hook, so it will create a new Engine!

Looks like that was added as a “Best Practice” on Feb 24th…
Update best practices · BabylonJS/Documentation@d93f66d (github.com)

2 Likes

Is there any update abot this topic or I just dont understand how to solve this problem?

@icy_director I ended up following the recommendations of others on this thread and completely separated my BJS scripts from react. I use the same scene component that I referenced from the documentation, but I wrap that in a page component that includes my babylon project, in this case an editor application:

// Components
import { EditorApplication } from "babylon-create/src/Applications/";
import EditorUI from "./EditorUI/EditorUI";
import SceneComponent from "components/babylon/SceneComponent";

export default function EditorPage(props) {
  const editor = new EditorApplication();
  const dispatch = useDispatch();

  const onSceneReady = (scene) => {
    editor.attachScene(scene);
  };

  const onRender = (scene) => {
    const profile = editor.profiler.get();
    dispatch(setSceneProfile(profile));
  };

  return (
    <Box sx={{ height: "100%" }}>
      <EditorUI setEditorRequest={editor.set} />
      <SceneComponent antialias onSceneReady={onSceneReady} onRender={onRender} id="viewport"></SceneComponent>
    </Box>
  );
}

By doing it this way, the onRender function is defined once and only once, and I have no useState functions in my page component. My UI for this page is a separate set of components that I pass my editor object’s set function to. My editor application object looks like following:

NOTE all babylonjs/ imorts here are my own scripts in a folder under that name and not part of BJS.

import Profiler from "babylonjs/profiler/Profiler";
import Actions from "babylonjs/actions/Actions";
import Inputs from "babylonjs/inputs/Inputs";

import { store } from "babylonjs/globals";
import { arcCamera } from "babylonjs/cameras/ArcCamera";
import { standardGrid } from "babylonjs/grids/StandardGrid";
import { standardEnvironment } from "babylonjs/environments/StandardEnvironment";

export default class EditorApplication {
  constructor() {
    this._scene = null;
    this.profiler = new Profiler();
    this.inputs = new Inputs();
    this.actions = new Actions();
  }

  attachScene(scene) {
    store.set("attachedScene", scene);
    this.camera = arcCamera(scene);
    this.grid = standardGrid(scene);
    this.environment = standardEnvironment(scene);
  }

  set(request) {
    if (action in request) {
      store.set("actionRequest", request);
    }
  }
}

This may be more complex than your use case, but the thing to look at is that I’m using a set method here to set any data inside my babylon object, I’m passing that editor.set function as a prop to my Editor UI and that’s how I’m passing data from React → Babylon. Also, I’m working on an editor type application for 3d experiences, so I don’t need to put animations and such in the onRender function, if you did need to do that I would create an onRender function in the Babylong object (similar to the way I’ve created the set function in my EditorApplication object) and then call that function in the onRender prop.

For getting information out of Babylon and into React I found I needed a different route entirely. I used redux here in my onRender defined in my component in the first block of code. I have a series of get functions I’ve made for information I want out of Babylon and I call and set those from my EditorPage onRender method using useDispatch to set them in redux. You could also likely do this with contexts as well. In all cases you want to make sure that the Page component never re-renders.

Hope that’s helpful! It’s a frustrating issue at first but once you wrap and organize your code I found it a much more pleasant dev experience separating my BJS project/code from react for interactive UI’s.

That’s only true if your EditorPage is not re-rendered (ie: props change)! Also, you are dispatching at the framerate - probably not good for performance :smile:

The onRender prop in the dependency array I think should be in a separate useEffect that updates the the engine.runRenderLoop. T-here’s no reason to tear down and create a new engine, because the onRender method has changed. I’m not sure what the etiquette is for undoing what somebody else considers a best practice, but would rather that person is on the forum to go through the changes for a discussion on best practices, because potentially they hadn’t considered that. I think likely what occured was those were added as dependencies to make the linting error go away. That could be carried forward that adaptToDeviceRatio should be a separate useEffect as well, but in practice I think it’s uncommon to change those properties.

Have you tried to use setState in class component, and use state in canvas?
Because it seems like it does not rerender the component, but updates the value.
I made similar components, functional and class, on every change in class component I dont get re-render. Ia m still woking on it so I am not really sure if this would be successful way to overcome it.

I think @brianzinn makes a great point, we don’t really know what the “best practices” for this specific topic are and we’re all figuring it out on the way. As with a lot of things, there are several ways to get to the desired outcome. These are also discrete problems that once you find a way to get what you want it probably just works and thus never bubbles up to a critical issue that would warrant a larger conversation in the community.

I haven’t tried setState in the component and useState in the canvas, but if it’s working for you that’s great! Stepping back, as a few folks have said here, useState is going to cause a re-render, on its component and every child of that component, so if you can avoid putting any reactive state above your scene component in the hierarchy then you should avoid a re-render. I don’t know enough about the inner workings of the canvas to know, but what you’re describing seems like it could be following the same principle by putting the useState in the canvas which is a child of the scene component thus not causing a re-render of the sceneComponent itself.

For my strategy in my editorPage I explicitly do not use any reactive states that would cause a re-render and I found that solved my original issue. I’m also using dispatch in my onRender because I’m pumping out performance data in realtime for debugging. Throttling that would make sense if you’re pushing out larger amounts of data for production. You could also potentially useContext with similar results.

Do you use useSelector somewhere? Where do you use it?
On using dispatch component will not re-render, but on useSelector change it will cause a re-render.
And what is .set? the first time I see that. You use set on store which confuses me, I can guess store is redux, but you said that’s function.

I ended up using redux toolkit, which is the latest best practice for redux as I understand it. This was the best way I found to get data out of Babylon and into react UI. Note: the “store” referenced in EditorApplication is not a react component but rather a separate local storage for my babylon application where values can be set and emmited to all listeners on in my separated Babylon code.

Everything in the EditorApplication component is pure Babylon code and it does not directly interact with the React UI. The EditorPage imports the EditorApplication, calls its’ methods, and connects them to UI components through useDispatch and by passing the “EditorApplication.set” method to the EditorUI component and children components in the props. I use useDispatch for all data coming out of my babylon code that needs to reach the React UI and I use EditorApplication.set() to pass data from the Editor UI (user inputs, clicking buttons, as JSON objects) to the Babylon code and a custom controller.

This works because all of my useSelectors are in a component that is a sibling component to my SceneComponent and not part of it. The Editor UI will keep re-rendering (which is what we want), while the SceneComponent in EditorPage will not rerender. These were the steps I took:

  1. I setup a redux slice for saving my scene profile data
  2. I added the slice to my redux store file that is at the src/ directory
  3. I useSelector in my EditorStats component that is a child of the EditorUI component
  4. I useDispatch in my EditorPage (above in previous posts) to push the result of my editor.profiler.get() method to redux

SceneProfileSlice.js

import { createSlice } from "@reduxjs/toolkit";

export const sceneProfileSlice = createSlice({
  name: "sceneProfile",
  initialState: {},
  reducers: {
    setSceneProfile: (state, action) => {
      state.fps = action.payload.fps;
      state.indices = action.payload.indices;
      state.faces = action.payload.faces;
      state.meshes = action.payload.meshes;
      //console.log(state)
    },
  },
});

// Action creators are generated for each case reducer function
export const { setSceneProfile } = sceneProfileSlice.actions;

export default sceneProfileSlice.reducer;

ReduxStore.js

import { configureStore } from "@reduxjs/toolkit";
import sceneProfileReducer from "./slices/SceneProfileSlice";


export default configureStore({
  reducer: {
    sceneProfile: sceneProfileReducer
  },
});

EditorUI.jsx (Child component of EditorPage)

import { useState } from "react";

// MUI
import Box from "@mui/material/Box";

// Components
import EditorMenu from "./EditorMenu";
import EditorQuickToolbar from "./EditorQuickToolbar";
import EditorStats from "./EditorStats";
import EditorLibrary from "./EditorLibrary/EditorLibrary";

export default function EditorUI(props) {
  const [openLibrary, setOpenLibrary] = useState(false);
  const [toolbarValue, setToolbarValue] = useState(null);

  const { setEditorRequest } = props;

  return (
    <Box>
      <EditorMenu />
      <EditorLibrary openLibrary={openLibrary} setOpenLibrary={setOpenLibrary} setToolbarValue={setToolbarValue} setEditorRequest={setEditorRequest} />
      <EditorQuickToolbar toolbarValue={toolbarValue} setToolbarValue={setToolbarValue} setOpenLibrary={setOpenLibrary} setEditorRequest={setEditorRequest} />
      <EditorStats />
    </Box>
  );
}

EditorState.jsx (Child of EditorUI)

// React
import { useSelector } from "react-redux";

// MUI
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";

// Icons
import CastConnectedOutlinedIcon from "@mui/icons-material/CastConnectedOutlined";
import DeviceHubOutlinedIcon from "@mui/icons-material/DeviceHubOutlined";
import DetailsOutlinedIcon from "@mui/icons-material/DetailsOutlined";
import ViewInArOutlinedIcon from "@mui/icons-material/ViewInArOutlined";

export default function EditorStats() {
  const sceneProfile = useSelector((state) => state.sceneProfile);
  const itemColor = "#dee4e4";
  const itemOffset = "10px";
  const iconOffset = "2px";

  return (
    <Box position="absolute" sx={{ display: "flex", flexDirection: "row", bottom: "10px", left: "10px", zIndex: (theme) => theme.zIndex.drawer + 1 }}>
      <Box>
        <Typography variant="body2" display="block" sx={{ color: itemColor, mr: itemOffset }}>
          <CastConnectedOutlinedIcon fontSize="inherit" style={{ position: "relative", top: iconOffset }} /> {sceneProfile.fps}
        </Typography>
      </Box>
      <Box>
        <Typography variant="body2" display="block" sx={{ color: itemColor, mr: itemOffset }}>
          <DeviceHubOutlinedIcon fontSize="inherit" style={{ position: "relative", top: iconOffset }} /> {sceneProfile.indices}
        </Typography>
      </Box>
      <Box>
        <Typography variant="body2" display="block" sx={{ color: itemColor, mr: itemOffset }}>
          <DetailsOutlinedIcon fontSize="inherit" style={{ position: "relative", top: iconOffset }} /> {sceneProfile.faces}
        </Typography>
      </Box>
      <Box>
        <Typography variant="body2" display="block" sx={{ color: itemColor, mr: itemOffset }}>
          <ViewInArOutlinedIcon fontSize="inherit" style={{ position: "relative", top: iconOffset }} /> {sceneProfile.meshes}
        </Typography>
      </Box>
    </Box>
  );
}

1 Like

Now I can change redux store on the page without re-render, I didnt know it would work like that.
But how do I pass it to my babylon .
LocalStorage will not update in my sceneComponent everytime I change useSelector. It will in localStorage, but not in sceneComponent.
You said you pass data from react to babylon by passing editor.set to EditorUi, but I need this data in my sceneComponent because I have imported mesh there and I need to change active geometry based on selected one by user.
That’s last thing I need to do in case to make the app works.
If it’s needed I could share my code so it would be esier to discuss.

Good questions. I am only using redux for getting data out of Babylon and to React, we’ll call it (B->R) here. For getting data from React to Babylon (R->B) I use editor.set(value) method which is just a callback to send the value, typically a string or json object, to the rest of my babylon code. There’s a complete separation between the two methods and the architecture you choose needs to reflect that. IE I don’t think it’s possible to use a single method for B->R and R->B or (B<>R)

It’s important to note here that EditorApplication has a direct reference to the scene in SceneComponent. So I have complete access to everything in my scene from EditorApplication. IE if I want to move a mesh in my scene from the UI I could send a json object of {"action":"moveMesh", "value":[1, 0, 0], "target":"sphere02"} to my EditorApplication using editor.set(JSON) then pass that to a function that calls Scene.Meshes(), searches for the mesh with name “sphere02” and adds the displacement from “value” to the meshes XYZ coordinates. I could speed up this process by creating an index of meshes in my scene as I add them, but that’s a performance question.

In your case, it sounds like you want to select a mesh name from a UI element and have it select/highlight a mesh in babylon. You could use a similar callback like editor.set() to pass a json object like {"action":"setSelected", "target":{selectedMeshName} to your EditorApplication. Now, when this is called you need logic to handle it in your EditorApplication, ie parse the action to a switch statement and run a method for SelectMesh(selectedMeshName:string) or something like that.

Yes, let’s definitely see some of your code here or you could post a link to a code sandbox / stackblitz. But I think we’re running into the territory of larger architectural questions. I’ll elaborate a little here on what I found to work.

I do not understand how I should write the set function and connect it with sceneComponent, but also to use that function in my scene which would bring data from react.
You said that store is local storage, but is it a js file which communicates with something which is closely working with scene, or it’s just a function doing all the job?

I commited this simulation. It’s very messy so all files I use now, are in babyblon folder, and I have redux in context folder.

I would really appreciate you for helping me.

OK, this is a great example. You have all your code inside you sceneComponent and once you’re scene initializes you have no way to interact with it. You need to pull all of that logic and create a state controller to manage your UI callbacks. Here’s a working example:

First, let’s remove all the logic from your scene component and add back in an onSceneReady

import React, { useEffect, useRef, useMemo } from 'react';
import { Engine, Scene } from '@babylonjs/core';
import * as BABYLON from '@babylonjs/core';
import {
  FreeCamera,
  Vector3,
  HemisphericLight,
  MeshBuilder,
} from '@babylonjs/core';
import { products } from '../../Info/products';
import { useSelector } from 'react-redux';

//const selectedObj = products.filter((e) => {
//  return e.id === 'G1677744748794';
//});

//const subProdFilter = selectedObj[0].subProducts.filter((e) => {
//  return e.id === 'S1677744811741';
//});
//const subProd = subProdFilter[0];

const SceneComponent = ({
  antialias,
  engineOptions,
  adaptToDeviceRatio,
  sceneOptions,
  // Added onSceneReady back in
  onSceneReady,
  onRender,
  state,
  ...rest
}) => {
  const reactCanvas = useRef(null);

  // const onSceneReady = (scene) => {
  //   var camera = new BABYLON.ArcRotateCamera(
  //     'camera1',
  //     Math.PI / 2,
  //     Math.PI / 2,
  //     100,
  //     new BABYLON.Vector3(0, 0, 0),
  //     scene
  //   );

  //   camera.setTarget(Vector3.Zero());

  //   const canvas = scene.getEngine().getRenderingCanvas();

  //   camera.attachControl(canvas, true);

  //   const light = new HemisphericLight('light', new Vector3(0, 1, 0), scene);

  //   light.intensity = 0.7;

  //   const marker = new BABYLON.TransformNode('marker');
  //   // marker.scaling = new BABYLON.Vector3(0.001, 0.001, 0.001);
  //   marker.position.y = -25;

  //   let modelParent;
  //   let modelMesh;

  //   BABYLON.SceneLoader.ImportMesh(
  //     '',
  //     '/scenes/',
  //     subProd.model,
  //     scene,
  //     (meshes) => {
  //       modelParent = meshes[0];
  //       modelParent.parent = marker;

  //       // modelMesh = meshes[2];
  //       modelMesh = scene.getMeshByName('front');

  //       var decalMaterial = new BABYLON.StandardMaterial('decalMat', scene);
  //       decalMaterial.diffuseTexture = new BABYLON.Texture(
  //         'logo512.png',
  //         scene
  //       );
  //       decalMaterial.diffuseTexture.hasAlpha = true;
  //       decalMaterial.zOffset = -2;
  //       decalMaterial.backFaceCulling = false;

  //       var onPointerDown = (evt) => {
  //         if (evt.button !== 0) {
  //           return;
  //         }

  //         var pickInfo = scene.pick(scene.pointerX, scene.pointerY, (mesh) => {
  //           return mesh === modelMesh;
  //         });
  //         if (pickInfo.hit) {
  //           var decalSize = new BABYLON.Vector3(5, 5, 5);

  //           var decalModel = BABYLON.MeshBuilder.CreateDecal(
  //             'decalModel',
  //             modelMesh,
  //             {
  //               position: pickInfo.pickedPoint,
  //               normal: pickInfo.getNormal(true),
  //               size: decalSize,
  //             }
  //           );
  //           decalModel.material = decalMaterial;
  //         }
  //       };
  //       canvas.addEventListener('pointerdown', onPointerDown, false);
  //     }
  //   );
  // };

  // set up basic engine and scene
  useEffect(() => {
    const { current: canvas } = reactCanvas;

    if (!canvas) return;

    const engine = new Engine(
      canvas,
      antialias,
      engineOptions,
      adaptToDeviceRatio
    );
    const scene = new Scene(engine, sceneOptions);
    if (scene.isReady()) {
      onSceneReady(scene);
    } else {
      scene.onReadyObservable.addOnce((scene) => onSceneReady(scene));
    }

    engine.runRenderLoop(() => {
      if (typeof onRender === 'function') onRender(scene);
      scene.render();
    });

    const resize = () => {
      scene.getEngine().resize();
    };

    if (window) {
      window.addEventListener('resize', resize);
    }

    return () => {
      scene.getEngine().dispose();

      if (window) {
        window.removeEventListener('resize', resize);
      }
    };
  }, [
    antialias,
    engineOptions,
    adaptToDeviceRatio,
    sceneOptions,
    onRender,
    onSceneReady,
  ]);

  return <canvas ref={reactCanvas} {...rest} />;
};

export default React.memo(SceneComponent);

Then let’s put all that logic into a new controller. In my previous examples this was my EditorApplication. For readability I’m changing it here to MyController.js

import * as BABYLON from '@babylonjs/core';
import { Vector3, HemisphericLight, PointerEventTypes } from '@babylonjs/core';
import { products } from '../../Info/products';

export default class MyController {
  constructor() {
    this._scene = null;
    this._store = {};
    this._pointerObserver = null;
  }

  attachScene(scene) {
    this._scene = scene;
    this._store['enabledMesh'] = 'front';
    this._pointerObserver = this.attachPointerObserver(scene);
    this.getModel();
    this.loadScene(scene);
    this.loadMaterials(scene);
  }

  // Callbacks used by react must be
  // an arrow function to maintain reference to "this"
  setEnabledMesh = (enabledMeshName) => {
    this._store['enabledMesh'] = enabledMeshName;
    console.log(
      'store is set to: ',
      enabledMeshName,
      this._store['enabledMesh']
    );
  };

  getModel() {
    const selectedObj = products.filter((e) => {
      return e.id === 'G1677744748794';
    });

    const subProdFilter = selectedObj[0].subProducts.filter((e) => {
      return e.id === 'S1677744811741';
    });
    const subProd = subProdFilter[0];

    this._store['model'] = subProd.model;
  }

  loadScene(scene) {
    var camera = new BABYLON.ArcRotateCamera(
      'camera1',
      Math.PI / 2,
      Math.PI / 2,
      100,
      new BABYLON.Vector3(0, 0, 0),
      scene
    );

    camera.setTarget(Vector3.Zero());

    const canvas = scene.getEngine().getRenderingCanvas();

    camera.attachControl(canvas, true);

    const light = new HemisphericLight('light', new Vector3(0, 1, 0), scene);

    light.intensity = 0.7;

    const marker = new BABYLON.TransformNode('marker');
    // marker.scaling = new BABYLON.Vector3(0.001, 0.001, 0.001);
    marker.position.y = -25;

    let modelParent;
    // Moving this logic down below
    //let modelMesh;

    BABYLON.SceneLoader.ImportMesh(
      '',
      '/scenes/',
      this._store['model'],
      scene,
      (meshes) => {
        modelParent = meshes[0];
        modelParent.parent = marker;

        // Moving this down below
        //modelMesh = scene.getMeshByName('front');

        // Moving this down below
        // var decalMaterial = new BABYLON.StandardMaterial('decalMat', scene);
        // decalMaterial.diffuseTexture = new BABYLON.Texture(
        //   'logo512.png',
        //   scene
        // );
        // decalMaterial.diffuseTexture.hasAlpha = true;
        // decalMaterial.zOffset = -2;
        // decalMaterial.backFaceCulling = false;
      }
    );
  }

  attachPointerObserver = (scene) => {
    if (!scene) return;
    const pointerObserver = scene.onPointerObservable.add((pointerInfo) => {
      if (pointerInfo.type != PointerEventTypes.POINTERDOWN) return;
      if (
        pointerInfo.pickInfo.pickedMesh &&
        pointerInfo.pickInfo.pickedMesh.name === this._store['enabledMesh']
      ) {
        const decalSize = new BABYLON.Vector3(5, 5, 5);
        const decalModel = BABYLON.MeshBuilder.CreateDecal(
          'decalModel',
          pointerInfo.pickInfo.pickedMesh,
          {
            position: pointerInfo.pickInfo.pickedPoint,
            normal: pointerInfo.pickInfo.getNormal(true),
            size: decalSize,
          }
        );
        decalModel.material = this._store['decalMaterial'];
      }
    });
    return pointerObserver;
  };

  loadMaterials(scene) {
    var decalMaterial = new BABYLON.StandardMaterial('decalMat', scene);
    decalMaterial.diffuseTexture = new BABYLON.Texture('logo512.png', scene);
    decalMaterial.diffuseTexture.hasAlpha = true;
    decalMaterial.zOffset = -2;
    decalMaterial.backFaceCulling = false;
    this._store['decalMaterial'] = decalMaterial;
  }
}

Your parent component that your react-router points to:

import React, { useEffect, useRef, useState } from 'react';
import EditorUi from './editor/EditorUi';
// import ModelL from './Asc';
import MyController from './MyController';
import SceneComponent from './SceneComponent';

const Parent = () => {
  const myController = new MyController();

  const onSceneReady = (scene) => {
    myController.attachScene(scene);
  };

  const onRender = (scene) => {
    // Nothing here, but could be added
  };

  return (
    <div>
      <EditorUi setEnabledMesh={myController.setEnabledMesh} />
      <div style={{ height: '70vh' }}>
        <SceneComponent
          onSceneReady={onSceneReady}
          onRender={onRender}
          antialias
          id="my-canvas"
          style={{ width: '100%', height: '100%' }}
        />
      </div>
    </div>
  );
};

export default Parent;

And lastly your UI element that sets EnabledMesh

import React from 'react';
import { products } from '../../../Info/products';

const selectedObj = products.filter((e) => {
  return e.id === 'G1677744748794';
});

const subProdFilter = selectedObj[0].subProducts.filter((e) => {
  return e.id === 'S1677744811741';
});
const subProd = subProdFilter[0];

const EditorUi = (props) => {
  const { setEnabledMesh } = props;

  return (
    <div>
      <div className="canvas-faces">
        {subProd.faces.map((e, index) => {
          return (
            <img
              onClick={() => {
                setEnabledMesh(e.faceName);
              }}
              key={index}
              className="face-img"
              src={e.picture}
            />
          );
        })}
      </div>
    </div>
  );
};


export default EditorUi;

This works. I wouldnt be able to solve this problem without you.
I will leave git repo in case someone else would have similar problem.
Thanks you for helping us.