In this tutorial, we will use the ever popular “counter” example. With a twist. Instead of using setTimeout
, we’ll be using the more performant requestAnimationFrame
method. This will seem challenging because requestAnimationFrame
is itself a closure. So how do we deal with closures inside of closures? How can we update the parent state of our Mithril component if the data we need is inside a nested closure?
Let’s get started by creating a reusable state outside our countDown
component:
<!DOCTYPE HTML>
<html>
<head>
<title>Mithril: Internal State Animation</title>
<link rel="stylesheet" href="style.scss" />
</head>
<body>
<div id="app"></div>
<script type="module" src="index.js"></script>
</body>
</html>
import m from 'mithril';
const stateObj = () => ({
count: 100,
requestRef: undefined,
previousTimeRef: undefined,
});
count
: represents the number we want to countdown from.requestRef
: will store a reference to the requestAnimationFrame function.previousTimeRef
: will store a reference to the current time frame from inside the animate function inside the requestAnimationFrame lexical scope.
Now let’s build out the countDown
component:
const countDown = () => {
// Initialize state object
let state = stateObj();
const animate = time => {
if( state.previousTimeRef !== undefined ){
const deltatime = time - state.previousTimeRef;
//Update count
state.count = state.count <= 0 ? state.count = 100 : (state.count - deltatime * 0.01) % 100;
//Redraw component after count is updated
m.redraw();
}
state.previousTimeRef = time;
state.requestRef = requestAnimationFrame(animate);
}
const startAnimation = () => {
state.requestRef = requestAnimationFrame(animate);
}
const stopAnimation = () => {
cancelAnimationFrame(state.requestRef);
state = stateObj();
}
return {
oncreate: () => {
startAnimation();
},
view: () => [
m('.number', Math.round(state.count)), //render updated count
m('button.stop',{
onclick: () => stopAnimation(),
},'Stop'),
m('button.start',{
onclick: () => startAnimation(),
},'Start'),
]
}
}
document.getElementById("app") && m.mount( document.getElementById('app'), {
view: vnode => m(countDown)
});
That wasn’t so hard. The reason we can keep track of the count is because object properties are passed by reference.
The startAnimation
function, and by extension the requestAnimationFrame
method, is only ran once because the oncreate
life cycle method is only ran once after the component is rendered, even after calling m.redraw()
after each count update.
Tutorial link: https://github.com/wlcdesigns/mithril-request-animation-frame
Tutorial inspired by: Using requestAnimationFrame with React Hooks