Babylon Viewer v2 and React

Here is very simple example of putting Babylon Viewer v2 into React

import React, { useEffect, useRef } from 'react';
import { Engine } from '@babylonjs/core';
import { Viewer } from '@babylonjs/viewer';

const ViewerOnlyTest: React.FC = () => {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    if (!canvasRef.current) return;

    const engine = new Engine(canvasRef.current, true);
    const viewer = new Viewer(engine);

    viewer.loadModel('https://playground.babylonjs.com/scenes/BoomBox.glb');
    viewer.onModelChanged.add(() => {
      console.log('Model changed');
    });

    return () => {
      engine.dispose();
    };
  }, []);

  return <canvas ref={canvasRef} style={{ width: '100%', height: '100%' }} />;
};

export default ViewerOnlyTest;

It works with no problems, but I have the next error in the concole:

If I dispose the viewer instance instead of engine, the error message changes ():

If I don’t dispose anything, there are no errors, but the model goes white and loads twice

Probably I missed some necessary hook?
What could be the best way to integrate the Viewer v2 into React?

if you turn off strict mode, it may behave more like you are expecting…
you can also put canvasRef in your dependency array - empty dependency array really means to run when loaded, so it would just show intent.

1 Like

Strict mode is off.

Made no difference.

The same code works without any issues in Preact.
So I wonder what I need do more to satisfy React in order not to load a model twice :slight_smile:

I haven’t seen effects running twice unless StricMode is on and that is by design. You would need to share a repro for me to see otherwise.

Also, canvasRef in dependency array is just to show intent. It wasn’t expecting a change.

1 Like

This strange error disappeared when I put the code into Stackblitz - https://stackblitz.com/edit/vitejs-vite-kvokne?file=src%2FViewerComponent.tsx,src%2FApp.tsx,src%2Fmain.tsx&terminal=dev

Still due to my extremely poor knowledge of React I have another question. Here - Vitejs - Vite (forked) - StackBlitz - I created the nice button which should toggle autorotation. But inspite all my efforts to pass the correct state it only fires once and doesn’t change anything. Hope that you’ll be able to help somehow.

it’s because your useEffect will only run once and the props that you have in your onBeforeRenderObservable are stale from the closure. You would want another useEffect with the props that change as dependencies.

edit:

// you need to pass the changing props to your component first:
<ViewerComponent
        orbit={orbitSpeed}
        source={'https://playground.babylonjs.com/scenes/BoomBox.glb'}
      />

Add a new useEffect or something your component to pick up those changes:

...
const viewerRef = useRef<Viewer | null>(null);
if (viewerRef.current) {
  viewerRef.current.cameraAutoOrbit = props.orbit;
}

useEffect(() => { 
  ...
  const viewer = ...
  viewerRef.current = viewer;
}, ...)

...

the problem with your observable was not react related - except you’d need to know that the props come in as a new object every time (not re-used like a useRef<>).

// you were effectively doing this - maybe that makes the stale closure clearer.
// this props object is not re-used by React.
const props = { orbit: true };

object.onBeforeRenderObservable.add(() => console.log(props.orbit))

// props object falls out of scope here and is never re-used
1 Like

Thank you very much!
My understanding now is better.
Here is the working example - Vitejs - Vite (forked) - StackBlitz

I found the function - createViewerForCanvas - which needs only canvas, even without engine. Will try to implement it and ask if failed :slight_smile:

1 Like

@ryantrem

Cool nice job! Couple notes:

  1. Regarding createViewerForCanvas, this was exactly the motivation for this layer. If we want additional viewer integrations with browser based frameworks (React being the prime example), createViewerForCanvas contains the common things needed. The React component would mostly be about providing a React-centric API and React-based UI layer (for things like the animation controls, etc.).
  2. Note that Web Components Custom Elements do work directly in React directly as well. See React DOM Components – React. As of React 18, if you are using TypeScript you do need some manual type declarations. Here is an example:
interface HTML3DElementAttributes
  extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> {
  src?: string;
  env?: string;
}

declare global {
  namespace JSX {
    interface IntrinsicElements {
      'babylon-viewer': HTML3DElementAttributes;
    }
  }
}

Even with this, there is still a downside in that you have the limitations of html elements, like attributes (translated to react properties) needing to be strings only. This can be unnatural in React, especially for common React cases like being able to pass a callback to a property.

2 Likes

I made the minimal example with createViewerForCanvas with WebGPU engine - Vitejs - Vite (forked) - StackBlitz
Very compact!

I found the way to use HTML3DElement without manual type declarations - CanvasViewer (forked) - StackBlitz

But with type declarations the code looks much simpler and nicer - CanvasViewer (forked) - StackBlitz

4 Likes

@labris Thank you for sharing your experience.

@ryantrem: Do we have it on the backlog to update the docs + CDN to have an example of createViewerForCanvas()? Would you accept a PR (@labris example)? It feels like this may go over the heads of newcomers on how well Viewer V2 can be utilised, with access to the Viewer ref. The layering is nicely done.

I have noticed a 3.1mb reduction from using Viewer V2 vs creating a minimal scene via babylonjs: import * as BABYLON from "babylonjs"
import { Viewer, createViewerForCanvas } from '@babylonjs/viewer' - 3.1mb :+1:

1 Like

I haven’t documented the lower layers (Viewer and createViewerForCanvas) since they are still marked as @experimental (the HTML3DElement layer is not marked as @experimental). It’s becoming increasingly unlikely that we will make significant changes to the lower layers, so the risk of wasted work documenting them is also going down.

There is already a doc page that briefly discusses using HTML3DElement in React and adding the type declarations here: Babylon.js docs

We could update that doc page to simply point to this thread as well for now. If we want to put an actual example of creating your own React component using createViewerForCanvas, we potentially could do that as well, although I would also want to add:

  1. Probably a general doc page about the lower level Viewer and createViewerForCanvas layers).
  2. Ideally add support to the Babylon docs website for embedding StackBlitz samples (assuming codepen.io doesn’t have React support that I just haven’t found).

We’d be happy to have docs contributions as well: GitHub - BabylonJS/Documentation: Babylon.js's documentation website

Cool! Just to make sure I’m understanding, you are saying that if you create and render a minimal scene based on the Babylon UMD package (e.g. import * as BABYLON from "babylonjs" + code using the BJS API) your bundle is 3.1mb larger than if you just use the new Babylon Viewer directly, correct? For the Viewer, we did a fair bit of work to make sure it only pulls in what is needed when it is needed, whereas the Babylon UMD package has pretty much every part of Babylon, so that sounds about right.

1 Like

These TypeScript declarations are helpful thank you.

Noted! I will happily try to help where I can find time.

Correct! I even included the UFO and env in the Viewer build but not the other so probably a better reduction. T’was minified, no source-maps. React.

1 Like