Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React: If you set state in a request animation frame will react schedule rendering on the animation frame?

I'm trying to understand the nuances of using something like a game loop inside of React (version 16+). I'm confused as to how React's rendering strategy conflicts (or doesn't conflict) with another rendering scheduler - in this case: request animation frame.

Refer to the following example where a game loop is used to set state:

class Loop extends Component {
  constructor(props) {
    super(props);
    this.state = { x: 0 };
  }

  componentDidMount() {
    let then = performance.now();
    const loop = now => {
      if (this.state.x < 400)
        window.requestAnimationFrame(loop);
      const dt = now - then;
      then = now;
      this.setState(prevState => ({ x: prevState.x + (dt * 0.1) }));
    };
    loop(then);
  }

  render() {
    const { x } = this.state;
    return <div style={{
      backgroundColor: "green",
      height: "50px",
      width: `${x}px`
    }}></div>;
  }
}

Will this work similarly to if one had manipulated the DOM directly? Or, will react do something like batch state updates to render, defeating the purpose of using a request animation frame?

like image 911
MFave Avatar asked Dec 30 '18 16:12

MFave


People also ask

Does react render after state change?

React components automatically re-render whenever there is a change in their state or props. A simple update of the state, from anywhere in the code, causes all the User Interface (UI) elements to be re-rendered automatically.

Can you set state in render react?

render() Calling setState() here makes it possible for a component to produce infinite loops. The render() function should be pure, meaning that it does not modify a component's state. It returns the same result each time it's invoked, and it does not directly interact with the browser.

What happens when state updates in react?

State updates in React are asynchronous; when an update is requested, there is no guarantee that the updates will be made immediately. The updater functions enqueue changes to the component state, but React may delay the changes, updating several components in a single pass.

Can we use state in render method?

You can't set react state in the render function, but you can in the constructor or most of the component lifecycle functions. Save this answer. Show activity on this post. Never set state inside render function as it might create a side effect and it will create an infinite render cycle eventually.


1 Answers

That's what I thought when answering this question: How to implement a gameloop with requestAnimationFrame across multiple React Redux components?

As setState is asynchronous, there's actually no guarantee that when calling it React is going to perform the update and re-render the component, on the contrary, React could just push that update to a queue that will process at some point, which might actually be in the next frame or later.

I profiled a really simple application using requestAnimationFrame to see if that was the case and actually it is not:

class ProgressBar extends React.Component {

  constructor(props) {
    super(props);
    
    this.state = {
      progress: 0,
    };
  }
  
  update() {
    this.setState((state) => ({
      progress: (state.progress + 0.5) % 100,
    }));
  }  

  render() {
    const { color } = this.props;
    const { progress } = this.state;
    
    const style = {
      background: color,
      width: `${ progress }%`,
    };
    
    return(
      <div className="progressBarWrapper">
        <div className="progressBarProgress" style={ style }></div>
      </div>
    );  
  }
}

class Main extends React.Component {

  constructor(props) {
    super(props);
    
    const progress1 = this.progress1 = React.createRef();
    const progress2 = this.progress2 = React.createRef();
    const progress3 = this.progress3 = React.createRef();
    
    this.componentsToUpdate = [progress1, progress2, progress3];
    this.animationID = null;    
  }
  
  componentDidMount() {  
    this.animationID = window.requestAnimationFrame(() => this.update());  
  }
  
  componentWillUnmount() {
    window.cancelAnimationFrame(this.animationID);
  }
  
  update() {
    this.componentsToUpdate.map(component => component.current.update());
  
    this.animationID = window.requestAnimationFrame(() => this.update());  
  }
  
  render() {
    return(
      <div>
        <ProgressBar ref={ this.progress1 } color="magenta" />
        <ProgressBar ref={ this.progress2 } color="blue" />     
        <ProgressBar ref={ this.progress3 } color="yellow" />       
      </div>
    );
  }
}

ReactDOM.render(<Main />, document.getElementById('app'));
body {
  margin: 0;
  padding: 16px;
}

.progressBarWrapper {
  position: relative;
  width: 100%;
  border: 3px solid black;
  height: 32px;
  box-sizing: border-box;
  margin-bottom: 16px;
}

.progressBarProgress {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
}
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="app"></div>

Here you can see how the updates are added to the queue (enqueueSetState), but the work is done straight away and all the calls to update, setState, render, commitWork... happen in the same frame:

Example app profiling result

However, I suspect that in a real application where React has more updates to handle or in future versions of React with features like time slicing, async updates with priorities... this might actually not be the case.

like image 112
Danziger Avatar answered Oct 28 '22 02:10

Danziger