Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React-Three-Renderer refs not current in componentDidUpdate (MVCE included)

I'm using react-three-renderer (npm, github) for building a scene with three.js.

I'm having a problem that I've boiled down to an MVCE. Refs aren't updating in the sequence I expect them to. First, here's the main code to look at:

var React = require('react');
var React3 = require('react-three-renderer');
var THREE = require('three');
var ReactDOM = require('react-dom');

class Simple extends React.Component {
  constructor(props, context) {
    super(props, context);

    // construct the position vector here, because if we use 'new' within render,
    // React will think that things have changed when they have not.
    this.cameraPosition = new THREE.Vector3(0, 0, 5);

    this.state = {
      shape: 'box'
    };

    this.toggleShape = this.toggleShape.bind(this);
  }

  toggleShape() {
    if(this.state.shape === 'box') {
      this.setState({ shape: 'circle' });
    } else {
      this.setState({ shape: 'box' });
    }
  }

  renderShape() {
    if(this.state.shape === 'box') {
      return <mesh>
        <boxGeometry
          width={1}
          height={1}
          depth={1}
          name='box'
          ref={
            (shape) => {
              this.shape = shape;
              console.log('box ref ' + shape);
            }
          }
        />
        <meshBasicMaterial
          color={0x00ff00}
        />
      </mesh>;
    } else {
      return <mesh>
        <circleGeometry
          radius={2}
          segments={50}
          name='circle'
          ref={
            (shape) => {
              this.shape = shape;
              console.log('circle ref ' + shape);
            }
          }
        />
        <meshBasicMaterial
          color={0x0000ff}
        />
      </mesh>
    }
  }

  componentDidUpdate() {
    console.log('componentDidUpdate: the active shape is ' + this.shape.name);
  }

  render() {
    const width = window.innerWidth; // canvas width
    const height = window.innerHeight; // canvas height

    var position = new THREE.Vector3(0, 0, 10);
    var scale = new THREE.Vector3(100,50,1);

    var shape = this.renderShape();

    return (<div>
        <button onClick={this.toggleShape}>Toggle Shape</button>
        <React3
          mainCamera="camera"
          width={width}
          height={height}
          onAnimate={this._onAnimate}>
          <scene>
            <perspectiveCamera
              name="camera"
              fov={75}
              aspect={width / height}
              near={0.1}
              far={1000}
              position={this.cameraPosition}/>
            {shape}
          </scene>
        </React3>
    </div>);
  }
}

ReactDOM.render(<Simple/>, document.querySelector('.root-anchor'));

This renders a basic scene with a green box, a fork of the example on react-three-renderer's github landing page. The button on the top left toggles the shape in the scene to be a blue circle, and if clicked again, back to the green box. I'm doing some logging in the ref callbacks and in componentDidUpdate. Here's where the core of the problem I'm encountering occurs. After clicking the toggle button for the first time, I expect the ref for the shape to be pointing to the circle. But as you can see from the logging, in componentDidUpdate the ref is still pointing to the box:

componentDidUpdate: the active shape is box

Logging in lines after that reveals the ref callbacks are hit

box ref null [React calls null on the old ref to prevent memory leaks]

circle ref [object Object]

You can drop breakpoints in to verify and to inspect. I would expect these two things to happen before we enter componentDidUpdate, but as you can see, it's happening in reverse. Why is this? Is there an underlying issue in react-three-renderer (if so, can you diagnose it?), or am I misunderstanding React refs?

The MVCE is available in this github repository. Download it, run npm install, and open _dev/public/home.html.

Thanks in advance.

like image 621
Scotty H Avatar asked Sep 21 '16 21:09

Scotty H


1 Answers

I checked the source in react-three-renderer. In lib/React3.jsx, there is a two phased render.

componentDidMount() {
    this.react3Renderer = new React3Renderer();
    this._render();
  }

  componentDidUpdate() {
    this._render();
  }

The _render method is the one that seems to loads the children - the mesh objects within Three.

_render() {
    const canvas = this._canvas;

    const propsToClone = { ...this.props };

    delete propsToClone.canvasStyle;

    this.react3Renderer.render(
      <react3
        {...propsToClone}
        onRecreateCanvas={this._onRecreateCanvas}
      >
        {this.props.children}
      </react3>, canvas);
  }

The render method draws the canvas and does not populate the children or invoke Three.

  render() {
    const {
      canvasKey,
    } = this.state;

    return (<canvas
      ref={this._canvasRef}
      key={canvasKey}
      width={this.props.width}
      height={this.props.height}
      style={{
        ...this.props.canvasStyle,
        width: this.props.width,
        height: this.props.height,
      }}
    />);
  }

Summarizing, this is the sequence:

  1. App Component render is called.
  2. This renders react-three-renderer which draws out a canvas.
  3. componentDidUpdate of App component is called.
  4. componentDidUpdate of react-three-renderer is called.
  5. This calls the _render method.
  6. The _render method updates the canvas by passing props.children (mesh objects) to the Three library.
  7. When the mesh objects are mounted, the respective refs are invoked.

This explains what you are observing in the console statements.

like image 70
vijayst Avatar answered Sep 27 '22 20:09

vijayst