Declarative react native

I was just reading up on babylon native on a medium post ( Babylon React Native: Bringing 3D and XR to React Native Applications | by Babylon.js | Medium) by @ryantrem

Specifically:

We also have some aspirations around declarative scene definitions using JSX (similar to GitHub - brianzinn/react-babylonjs: React for Babylon 3D engine) as React Native has some great fast refresh optimizations when only JSX is changed, but no concrete plans here yet.

The template I am comparing to below is here:
BabylonReactNative/App.tsx at master · BabylonJS/BabylonReactNative (github.com)

So, I was able to quite quickly get it working. One thing I noticed was it allowed me reduce the template code in some parts. I didn’t need hooks to track instances - look at how I scale the transform node (I do not need to track the transform node instance or require a useEffect to apply the scaling) - react-reconciler does that via the renderer. It would work the same with the Model and a useBeforeRender hook to match the original template - just experimenting here…

Full code:

/**
 * Modified the TypeScript template
 * https://github.com/react-native-community/react-native-template-typescript
 */

import React, { useState, FunctionComponent, useEffect, useCallback } from 'react';
import { SafeAreaView, StatusBar, Button, View, Text, ViewProps, Image } from 'react-native';

import { EngineView, useEngine } from '@babylonjs/react-native';
import { Camera, Vector3, Color3, TransformNode } from '@babylonjs/core';
import { EngineCanvasContext, Scene } from 'react-babylonjs';
import Slider from '@react-native-community/slider';

const EngineScreen: FunctionComponent<ViewProps> = (props: ViewProps) => {
  const defaultScale = 1;

  const engine = useEngine();
  const [toggleView, setToggleView] = useState(false);
  const [camera, setCamera] = useState<Camera>();
  const [scale, setScale] = useState<number>(defaultScale);

  return (
    <>
      <View style={props.style}>
        <Button title="Toggle EngineView" onPress={() => { setToggleView(!toggleView) }} />
        { !toggleView &&
          <View style={{flex: 1}}>
            <EngineCanvasContext.Provider value={{ engine, canvas: null }}>
              {engine &&
                <Scene>
                  <arcRotateCamera
                    name="camera1"
                    onCreated={camera => setCamera(camera)}
                    target={Vector3.Zero()}
                    alpha={Math.PI / 2}
                    beta={Math.PI / 4}
                    radius={8}
                  />
                  <hemisphericLight
                    name="light1"
                    intensity={0.7}
                    direction={Vector3.Up()}
                  />
                  <transformNode name='Root Container' scaling={new Vector3(scale, scale, scale)}>
                    <icoSphere
                      name={"ico"}
                      radius={0.2}
                      flat
                      subdivisions={1}
                    >
                      <standardMaterial
                        name={'ico-mat'}
                        diffuseColor={Color3.Red()}
                        specularColor={Color3.Black()}
                      />
                    </icoSphere>
                  </transformNode>
                  
                </Scene>
              }
            </EngineCanvasContext.Provider>
            <EngineView camera={camera} displayFrameRate={true} />
            <Slider style={{position: 'absolute', minHeight: 50, margin: 10, left: 0, right: 0, bottom: 0}} minimumValue={0.2} maximumValue={2} step={0.01} value={defaultScale} onValueChange={setScale} />
            <Text style={{color: 'yellow',  position: 'absolute', margin: 3}}>react-babylonjs</Text>
          </View>
        }
        { toggleView &&
          <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
            <Text style={{fontSize: 24}}>EngineView has been removed.</Text>
            <Text style={{fontSize: 12}}>Render loop stopped, but engine is still alive.</Text>
          </View>
        }
      </View>
    </>
  );
};

There is a new react-babylonjs website coming out for 5.0 - will be adding recipes like this and more. Just need to make some time!! The new website is cool, because all the samples open in code sandbox and will be easy to contribute content via markdown - on the site itself you can switch between the running sample and code (much like our sandbox), but it also has javascript and typescript for all examples automatically via transpiling. Looking forward to rolling that out soon. :smiley:

edit: tried to change code to jsx syntax, but looks like it doesn’t work :cry:

3 Likes

From my experience, exclusively using declarative React JSX for Babylon has greatly reduced bugs and improved performance by many folds.

However, for production apps/games, I would strongly recommend class based React PureComponent, instead of hooks, for these reasons:

  • Caching: when you pass onCreated={camera => setCamera(camera)} you create a new function each time, thus causing the <arcRotateCamera to always re-render. Instead, do this:
export class Scene extends PureComponent {
   setCamera = (camera) => this.setState({camera})
   render () {
    return (
     <arcRotateCamera onCreated={this.setCamera} />
   )
  }
}

=> for small demos, the effect is barely noticable, but on complex scenes, you begin to see the real difference between laggy 3D scene and butter smooth experience.

  • Composition: a lot of times, you need to coompose different babylon React components using adhoc logic → a class based React component allows you to easily integrate them together using this scope. Example:
export class Scene extends PureComponent {
   onMountCamera = (camera) => this.camera = camera
   onClickZoomButton = () => this.camera.radius *= 0.5
   render () {
    return (<>
     <arcRotateCamera onCreated={this.onMountCamera} />
     <button onClick={this.onClickZoomButton}>Zoom In</button>
   </>)
  }
}
4 Likes

cc @ryantrem :slight_smile:

What I had originally posted certainly doesn’t follow best practices, but it is often easier to follow the gist that way. I’ve worked on large production React applications, so can appreciate the importance of performance. My examples for the last few years always use hooks, since all of the React docs switched from classes to hooks.

I think your first one is the same as this:

const Scene = React.memo(() => {
  const [camera, setCamera] = useState();
  const onCameraCreated = useCallback(
    (camera) => setCamera(camera),
    [setCamera]
  );
  return (
   <arcRotateCamera onCreated={onCameraCreated} />
  )
})

Some people like classes while other prefer hooks. Not to start a discussion about that in this thread, but I think the caching and performance benefits are available either either way, so it boils down more to user preference. I encourage people to work with what they like the most :smile:

@brianzinn this is awesome! Some questions:

  1. Did you have to make any changes to Babylon React Native to make this work, or does it work out of the box with what you have above?
  2. Having the EngineCanvasContext and the EngineView as siblings in the jsx seems a little awkward. Would it also work to have something like:
<EngineCanvasContext.Provider value={{ engine, canvas: null }}>
  <EngineView camera={camera} displayFrameRate={true}>
    <Scene>
      <arcRotateCamera
        name="camera1"
        onCreated={camera => setCamera(camera)}
        ...
      />
      ...
    </Scene>
  </EngineView>
</EngineCanvasContext>
  1. If the above doesn’t work, are there changes we could make to @babylonjs/react-native and/or react-babylonjs to make something like this possible? Or do you have any other ideas for making this more natural?
1 Like

I agree, it’s mostly preference.

However, there is a difference - using React hooks and useCallback pattern produces bloated code that’s hard to scan-read (i.e. speed reading code). Because you have to declare it twice - inside useState, and then inside useCallback. Then you have to think of what props to use as the second argument for useCallback - also bloated code, most of the time.

Using React class also allows you to skip useState declaration and setState on the fly, which Hooks cannot give you. (though I know some might say that it is not a good pattern - usually folks, who favor static languages :slight_smile: )

Definitely agree that there are strong opinions on both sides of the fence. I myself do not like to use this as I find that harder to reason with. I find it harder to scan-read the Classes now - probably because I stopped using them when react 17 alphas came out, so I’m out of practice :slight_smile: I don’t necessarily favour static languages, but I do like it when my compiler and IDE help me out!! Thanks for sharing.

No changes needed anywhere.

Yes, you could also make a HoC or some other convenience. This is completely untested - just writing here freestyle:

export type SceneContainer = {
  // engine: Nullable<Engine>, // could pass this in instead
  children: React.ReactNode
}
const SceneContainer: FC<SceneContainer> = ({children}) => {
  const [camera, setCamera] = useState();
  const engine = useEngine();
  // see previous code to create onCameraCreated callback
  return (
    <EngineCanvasContext.Provider ...>
    <>
      <EngineView camera={camera} displayFrameRate={true}>
      {engine &&
        <Scene onCameraAddedObservable={onCameraCreated}>{children}</Scene>
      }
   </>
  </EngineCanvasContext.Provider>
  )
_

Then in the calling code just go:

const MyScene = () => (
  <SceneContainer>
    <arcRotateCamera .../>
    <hemisphericLight ... />
    <YourComponentsHere />
  </SceneContainer>
)

You would want to add ways to pass engine params or scene params/callbacks, but that’s one way to make less awkward/more natural.

I don’t think any changes are needed. Something like above or a function to encapsulate the whole thing to glue them together. You don’t need to render a canvas, so on the one hand it’s a bit easier than web, but you need to awkwardly hand the camera back. I split out the EngineView as it’s own component a while back for multi-canvas - if I thought about it more might be able to work those together, but my understanding of native is it’s a single engine/single view/etc.

edit: you can just npm i react-babylonjs in playground and original snippet works OOTB. I will do an update to main library for 5.0 peer deps, but for now you need to --force or use legacy deps flag on the RC.

I totally agree, people have very strong opinions, so I do not want to get into that debate either. Just want to point out facts for other folks who might be reading this and do not have enough React experience to tell the difference.

Putting personal preferences aside (i.e. not taking IDE into account, because with Webstorm for example, there is no difference), here are factual differences to consider:

PureComponent
Pros

  • Performance: simple caching + less code + faster runtime code evaluation
  • Composition: access to this scope for easier integration between Babylon components
  • Intuitive Lifecycles: example: componentDidMount => scene.render(), componentWillUnmount => scene.dispose()

Cons

  • Lacks easy granular caching control - you have to write logic in Lifecycles
  • Requires decorators or class extension for code reuse

Hooks
Pros

  • Reusable chunks of logic, benefiting from functional approach
  • Granular caching control: example: use custom props combination to force re-render automatically

Cons

  • Performance: caching props requires extra code + useCallback/useEffect/etc. consumes extra CPU power to evaluate the arguments (or construct new functions) each time for each prop
  • No this scope, which makes it hard to work with Babylon’s OOP approach
  • No easy to reason about lifecycles - you have to dig into the entire code to figure out the flow.

Feel free to add/suggest updates to this list, I will probably write up an article about this some point in the future.

P.S. Just did a quick google search, I’m shocked at the misinformation you get from people comparing React class vs hooks - it’s just blatantly incorrect. A lot of downsides from the initial React class design back in 2013 is gone - with ES6/7/8 today, only good stuff is left.

2 Likes

Look forward to reading that! In the case of Babylon.js (ie: you won’t see people using createElement on DOM React!) the declarative markup can reduce code and bugs by how it glues things together - the reconciler handles it with a bit of magic :fairy: :rainbow: :shamrock: :magic_wand:

1 Like

Meanwhile, here is a great article pointing the exact performance issues with React in general, and particularly, hooks: Virtual DOM is pure overhead.

For most apps in the world, the performance penalty is negligible, so the trade off for convenience in developer experience is worth it.

But this is not the case with Babylon, because I’ve seen too many sluggish experiences from the official demo page of Babylon when people misuse it with React.

If you build a Babylon app that is more than just a quick demo MVP, it’s worth switching away from React Hooks. But better, to look into Svelte when this thing is more mature (I predict it will take over React, since it is now officially funded by Next.js’ parent company).

I am way too late for the party, but for posterity, your example can be simplified.

First, (camera) => setCamera(camera) can be simplified to just setCamera.

Second, setters are stable (check the note) and no need to pass them as dependencies to useCallback.

And third, because of the first point, there’s no need to use useCallback, at all. There’s no closure to cache, thus:

const Scene = React.memo(() => {
  const [camera, setCamera] = useState();
  return <arcRotateCamera onCreated={setCamera} />;
});
2 Likes