Categories
Tutorials

Updating the Internal State of a Mithril Component

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

Leave a Reply

Your email address will not be published. Required fields are marked *