<babylon> tag doesn't work with React

Hi there,

I would like to use the babylon viewer npm package in my react project. However React complains about the recommended <babylon> tag.

Is there a correct practice of using it in react?

My current code looks like this:


// I need this otherwise TSlint complains already that the `Property 'babylon' does not exist on type 'JSX.IntrinsicElements'`
declare global {
  namespace JSX {
    interface IntrinsicElements {
      babylon: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
    }
  }
}


export const FurnitureThumbnail: React.FC<{ furnituretype: IFurnitureType }> = ({ furnituretype }) => {
  useEffect(() => {
    BabylonViewer.viewerManager.getViewerPromiseById(furnituretype._id).then(function (viewer) {
      // this will resolve only after the viewer with this specific ID is initialized
      viewer.onEngineInitObservable.add(function (scene) {
        viewer.loadModel({
          title: 'Helmet',
          subtitle: 'BabylonJS',
          thumbnail: 'https://www.babylonjs.com/img/favicon/apple-icon-144x144.png',
          url: 'https://www.babylonjs.com/Assets/DamagedHelmet/glTF/DamagedHelmet.gltf',
        });
      });
    });
  }, [furnituretype]);

  return <babylon id={furnituretype._id}></babylon>;
};

The error message is:

Thanks in advance for any help!
Best regards,
Chen

The viewer was not designed to be intergrated within react, as the tag is not a react component (but a pseudo-html element). There is a react integration (GitHub - brianzinn/react-babylonjs: React for Babylon 3D engine) by the amazing @brianzinn, i would recommend using this one instead of the viewer, if using react.

1 Like

Thanks @RaananW ! It’s a pity that I cannot use it in react. Will loop up the package you mentioned.

Best,
Chen

1 Like

I imagine you could get around that element limitation in react by using a ref to a div and then doing some DOM manipulation like appendChild to your ref node. Another way is with a React portal, but I haven’t tried either of those myself with the viewer.

Another thing worth trying is to create the elements yourself:

React.createElement('babylon', {id:furniture_type._id});

Just need to make sure on re-renders that you aren’t creating new DOM elements. I’m really busy today, but if you want to go that route we could probably figure out a way on code sandbox. The playground isn’t setup for React.

I would be very careful with that, as the viewer is changing the dom when loaded.

Hi @brianzinn, I’ve tried with both the appendChild and React.createElement methods. The results are the same as before:

The tag <babylon> is unrecognized in this browser...

And I’m not sure how to setup a codepen with it, as I’m developing using CRA (create-react-app). Codepen doesn’t seem to work with a node app.

Sorry @ccc159 I can try more later, but am really busy with work today. Codepen does work with React - you can use a script in the html or use babel as the js preprocessor. I was using code sandbox and it was clearing the body just loading the viewer. I can try with CRA tonight.

Sorry again @ccc159 - I thought the viewer worked on dynamic HTML, but I don’t think the viewer works with dynamically added DOM (I may be wrong). Here was my test case in CRA:

import React, { useRef, useEffect } from 'react';
import './App.css';

function App() {

  const divRef = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    if (divRef.current) {
      divRef.current!.innerHTML = '<babylon model="model.gltf"></babylon>'
    }
  }, [divRef])

  return (
    <div className="App">
      <header className="App-header">
        header here
      </header>
      <div ref={divRef}></div>
    </div>
  );
}

console.log(((window as any).BabylonViewer as any)!.viewerManager);

((window as any).BabylonViewer as any)!.viewerManager.onViewerAddedObservable.add(function(viewer: any) {
  console.log('viewer', viewer);
})

const observer = new MutationObserver((mutationsList, observer) => {
  for(const mutation of mutationsList) {
    if (mutation.type === 'childList') {
        const addedNodes = mutation.addedNodes as NodeList;
        if (addedNodes.length > 0 && addedNodes[0].nodeName.toLowerCase() === 'babylon') {
          console.log('babylon node detected', mutation.addedNodes);
          observer.disconnect();
        }
    }
}
});
observer.observe(document.body, {attributes: true, childList:true, subtree:true})

export default App;

image

This does work, though :slight_smile:

useEffect(() => {
    if (divRef.current) {
      // you may need to clean that up on unmount as well as make sure it's not called again
      let viewer = new (window as any).BabylonViewer.DefaultViewer(divRef.current, {
        scene: {
            debug: true
        },
        camera: {
            behaviors: {
                autoRotate: 0
            }
        },
        model: {
            url: "https://playground.babylonjs.com/scenes/Rabbit.babylon"
        }
    });
    }
  }, [divRef])
1 Like

Hi @brianzinn Thanks for your effort! This worked :slight_smile:

If I understand it correctly, it bypasses the usage of <babylon> tag and instantiate directly a viewer?

Seems a clean solution to use in React. It would be great if this could be documented on the viewer readme.

@ccc159 @RaananW there are a lot more examples on the main site. I got the example from here:

You can always do a PR to link the viewer readme to the docs :open_book:

Thanks :slight_smile:

I wrote the viewer and its documentation. I am still not sure this is the best course of action for react applications, but it will work for sure. There might be a few issues when destructing the component, since some dom elements might be manipulated, and react will not like it one bit.

I am very happy every time the viewer is used, I still think that it is not 100% ready for react applications :slight_smile:

The same reason you need a react renderer to many different applications - react requires react components instead of pure html manipulations.

But again - as long as it works, I am very happy :slight_smile:

2 Likes

Yes before the 100%-react-ready babylon viewer, I would implement it like this. Or you rather think there are better practices to use the viewer in react? How would you compare GitHub - brianzinn/react-babylonjs: React for Babylon 3D engine with @brianzinn 's approach?

Thanks for your amazing work btw :slight_smile:

Yes, thanks Raanan for your work on the viewer - I did know that you wrote it :slight_smile: The model code in react-babylonjs was originally inspired and then parts borrowed from the viewer - how models are loaded and attached to a root parent, cleanup, etc… So, the actual model loading code across the different projects has more in common that you might think.

@ccc159 - I’ll try to offer a comparison, since you asked.
For me to compare them would be more on the tighter integration to React offered by a specialized integration, whereas the Viewer is more portable and easier to setup, configure and use.

A viewer basic scene may end up something like this:

The React equivalent code for the default viewer would then be something like this:

function reactModelViewer(props) {
  let baseUrl = 'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/'
  return (
    <Engine antialias adaptToDeviceRatio canvasId='babylonJS'>
      <Scene>
        <arcRotateCamera name='arc' target={new Vector3(0, 1, 0)}
          alpha={-Math.PI / 2} beta={(0.5 + (Math.PI / 4))}
          radius={2} minZ={0.001} wheelPrecision={50}
          useAutoRotationBehavior
        />

        <directionalLight name='dl' direction={new Vector3(0, -0.5, 0.5)} position={new Vector3(0, 2, 0.5)}>
          <shadowGenerator mapSize={1024} useBlurExponentialShadowMap blurKernel={32} shadowCastersExcluding={['gazeTracker', 'BackgroundHelper', 'BackgroundPlane', 'BackgroundSkybox']} />
        </directionalLight>

        <model
          position={props.position}
          rootUrl={`${baseUrl}BoomBox/glTF/`} sceneFilename="BoomBox.gltf"
          scaling={props.scaling}
        />
        <vrExperienceHelper webVROptions={{ createDeviceOrientationCamera: false }} teleportEnvironmentGround enableInteractions />
        <environmentHelper options={{ enableGroundShadow: true /* true by default */, groundYBias: 1 }} setMainColor={[Color3.FromHexString('#74b9ff')]} />
      </Scene>
    </Engine>
  )
}

There’s another way to load models using hooks and React.Suspense, but that’s the declarative way. Notice that the model position is controlled by props, so you could have DOM buttons in your React application to control the model scaling and position (or any properties, even lights, etc.). Those props will flow directly to your BabylonJS objects from React state. You can also do things outside of the React render directly on BabylonJS and compose your scene graph in more customizable ways…

I’m glad we could get the viewer running in React - if you are happy with that setup then that is great! If you have specific questions ask away!!

1 Like

@brianzinn Thanks for your explanation! Also the react-babylonjs example looks really neat! So far I am happy with the setup. Great work :relaxed:

2 Likes