Struggling to think of the best way to go about it. I can use a recursive call with requestAnimationFrame
to have a game loop:
export interface Props {
name: string;
points: number;
onIncrement?: () => void;
onDecrement?: () => void;
}
class Hello extends React.Component<Props, object> {
constructor(props: Props) {
super(props);
}
render() {
const { name, points, onIncrement, onDecrement } = this.props;
return (
<div className="hello">
<div className="greeting">
Hello {name + points}
</div>
<button onClick={onDecrement}>-</button>
<button onClick={onIncrement}>+</button>
</div>
);
}
componentDidMount() {
this.tick();
}
tick = () => {
this.props.onIncrement();
requestAnimationFrame(this.tick)
}
}
But what if I want on each frame:
I could just have another loop in each component, however my understanding is it is bad practice to have multiple requestAnimationFrame
loops going and it is a significant performance hit.
So I'm at a lost here. How can I have another component use the same loop? (If that even is the best way to go about it!)
You need a parent component that calls requestAnimationFrame
and iterates over an array of refs
of the children components that need to be updated on every cycle, calling its update
(or however you want to call it) method:
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>
Keep in mind, though, that if you are trying to do something too complex and want to hit 60 fps
, React might not be the right tool to use.
Also, setState
is asynchronous, so when calling it you are just pushing an update to a queue that React will process at some point, which might actually happen in the next frame or later.
I profiled this simple example to see if that was the case and it's not, actually. The updates are added to the queue (enqueueSetState
), but the work is done straight away:
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... the rendering might actually happen in a different frame.
One solution would be to define an array such as callbacks
as part of your state. At the beginning of each component's lifecycle, add a function to this array which does what you want each loop. Then call each function in your rAF loop like this:
update( performance.now())
// Update loop
function update( timestamp ) {
// Execute callback
state.callbacks.forEach( cb => cb( ...args )) // Pass frame delta, etc.
requestAnimationFrame( update )
}
With some work you can adjust this simple example to provide a way to remove functions from callbacks
to allow dynamic adding/subtracting routines from the game loop by name or signature.
You could also pass an object wrapping the function which also contains an integer that you could use to sort the callbacks by priority.
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