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?
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.
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.
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.
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.
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:
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With