orbitControls in React kills render

HI- I’m having a hard time adding orbitControls to a React component that uses THREEjs and was previously rendering correctly.

In my Viewport React component, I import orbitControls like this:

import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

and looking at IntelliSense, it seems to be imported OK.

I have two rendering methods: renderCanvas and a new renderAnimatedCanvas like this:

renderCanvas() {

    this.state.renderer.render(this.state.scene, this.state.camera);

  }

  renderAnimatedCanvas() {

    // "top" rendering call
    this.state.renderer.render(this.state.scene, this.state.camera);

    let renderer = this.state.renderer;
    let camera = this.state.camera;
    let scene = this.state.scene;

    // "problem" this next line prevents rendering from both "top" and "bottom" calls
    const controls = new OrbitControls(camera, renderer.domElement);

    var animate = function () {
      // requestAnimationFrame(animate);
      // controls.update();
      render();
    }

    function render() {
      renderer.render(scene, camera)
    }

    animate();

    // "bottom" rendering call
    renderer.render(scene, camera);

  }

renderCanvas has always worked and continues to work when called. But when I swap renderAnimatedCanvas in its place, it will never render correctly when the control initialization line is not commented out. (Just below the “problem” comment.) If this line is commented out, the render will work either at the “top” call or at the “bottom” call. But when this “problem” line is left in, neither render call works, even the one at the “top”.

I figured to someone more experienced the error would be obvious. Thanks for any suggestions!

  1. Could you explain why it’s necessary for you app to call render 5 times in 5 different configurations :thinking: ? Since animate is repeated on every frame, it shouldn’t be necessary to call renderer.render elsewhere (it will work, but it’ll just trigger an unnecessary re-render.)
  2. Make sure the renderAnimatedCanvas method you shared isn’t called more than once by React - otherwise you’re recreating the render loop and controls (which at this point may be bound to window event handlers and reacting on the canvas that was removed during the component update.) Both the animation loop and the controls should, in most cases, be created only once.

I should have explained that I put the extra render statements in renderAnimatedCanvas() to try to isolate the problem. The essential method is like this:

renderAnimatedCanvas() {
    let renderer = this.state.renderer;
    let camera = this.state.camera;
    let scene = this.state.scene;

    // "problem" this next line prevents rendering called in animate function
    const controls = new OrbitControls(camera, renderer.domElement);

    var animate = function () {
      requestAnimationFrame(animate);
      controls.update();
      render();
    }

    function render() {
      renderer.render(scene, camera)
    }

    animate();
  }

Whenever “problem” line (and controls.update) are included, I can tell the scene is being loaded properly but nothing renders.

renderAnimatedCanvas() (or, previously, renderCanvas()) is called once at the end of every componenentDidUpdate lifecycle hook. This is called whenever an element of the component state changes. I know componentDidUpdate is called 5 or six times as the scene elements are loaded, but renderAnimateCanvas shouldn’t force additional componentDidUpdate's

So you kick off a rendering loop each time your react component changes? This is really hard to debug without at least seeing the entire react component. You should probably instantiate OrbitControls on something like componentDidMount not on update.

That makes sense to try. Can you think of anything wrong with making orbitControls part of the component state, so that renderAnimatedCanvas can access it like:

let controls = this.state.orbitControls;

?

1 Like

@dubois: that worked with just a little fiddling! Thanks so much.

I had to reset the controls target to the scene center because for some reason it came in at 0,0,0. And my scene is too massive to orbit easily of course but I’ll have fun making that work.

For anyone else using React, this is what finally worked…

class Viewport extends Component {
  constructor(props) {
    super(props);

    this.state = {
      scene: new THREE.Scene(),
      camera: new THREE.PerspectiveCamera( 14, window.innerWidth / window.innerHeight, 10, 20000 ),
      renderer: new THREE.WebGLRenderer(),
      controls: null,
    };
  }

  componentDidMount() {
    this.setupCanvas();
    this.setState({ controls: new OrbitControls(this.state.camera, this.state.renderer.domElement) })
  }

  componentDidUpdate() {
    this.reviseCanvas();
    if (!this.props.loadingUnderway) {
        this.renderCanvas()
    } else this.renderAnimatedCanvas();
  }

and then…

renderAnimatedCanvas() {
    let renderer = this.state.renderer;
    let camera = this.state.camera;
    let scene = this.state.scene;
    let controls = this.state.controls;

    controls.target = new THREE.Vector3(this.props.sceneCenter.x, this.props.sceneCenter.y, this.props.sceneCenter.z);

    var animate = function () {
      requestAnimationFrame(animate);
      controls.update();
      console.log(controls)
      render();
    }

    function render() {
      renderer.render(scene, camera)
    }

    animate();
  }

@NoahK having canvas / controls / render loop initialization in componentDidUpdate is a very risky move - it may work, but when it will stop working you’ll likely spend a week trying to figure out why - you can already see that for some reason I had to reset the controls target to the scene center because [...] came in at 0,0,0 is happening, and it’s hard to figure out where and why.

As @dubois said - instantiate things that should be created only once (ie. renderer / camera / scene / controls / animation loop etc.) in enter the constructor (as private props, not as a state - state tends to update, and they shouldn’t), or in componentDidMount. In componentDidUpdate you can then update / toggle them as per need.

If that’s an option at the state of your project, you may also consider react-three-fiber - it’ll take care of this updates / disposal / refreshes automatically for you. :sweat_smile:

I don’t think that this is an antipattern. It’s not uncommon to store the reference to the renderer as context or in redux or something along those lines.

It would make sense to setState({camera:someOtherCamera}) for example more than the renderer though. So to your point, it also wouldnt hurt to instantiate these on the component itself this.camera.

The target thing though does seem risky, again there is a ton of code missing to figure out what was even supposed to happen to that camera and its controls in the first place.

1 Like