Building a metronome: React vs Alpine
Updated: in 50ad7ce
Lately, I’ve been learning how to use Alpine.js, a minimal JavaScript framework ideal for adding interactivity to static websites. The framework is awesome. It lets you add reactive behavior directly on your markup by using HTML attributes.
The goal of this article is to compare React and Alpine by building a simple metronome. We’re going to take a very simple approach, so keep in mind that the metronome we’re building is not going to be the best. If you want to implement time-based or sound-related behavior, a better approach would be to make use of the Web Audio API. For now, we’re going to keep it simple, as our main goal is just to try and compare the two frameworks.
React
Let’s try the metronome. Click the start button. You should be able to listen a click that repeats over time. You can change the speed by manipulating the range input.
Now let’s take a look at the code.
const Metronome = () => {
const { bpm, setBpm, isPlaying, setIsPlaying } = useMetronome();
return (
<div>
<div>
<div>{bpm} BPM</div>
<input type="range" min="60" max="240" value={bpm} onChange={event => setBpm(event.target.value)} />
</div>
<button onClick={() => setIsPlaying(!isPlaying)}>{isPlaying ? "Stop" : "Start"}</button>
</div>
);
};
We’re using a custom hook called useMetronome
that is returning the following variables:
bpm
: Beats per minute. It sets the speed of the repeating click sound.isPlaying
: It sets whether the audio is playing or not.setBpm
andsetIsPlaying
: Functions to update the previous values.
We’re updating bpm
with the range input and isPlaying
with the button.
Let’s now take a look at the useMetronome
hook.
const useMetronome = () => {
const [bpm, setBpm] = useState(120);
const [isPlaying, setIsPlaying] = useState(false);
useEffect(() => {
let timer;
const click = new Audio("/audio/click.wav");
if (isPlaying) {
timer = setInterval(() => click.play(), (60 / bpm) * 1000);
}
return () => clearInterval(timer);
}, [bpm, isPlaying]);
return { bpm, setBpm, isPlaying, setIsPlaying };
};
Almost all the action happens inside useEffect
. We’ll need to set an interval to be able to repeat the sound over
time, declare a timer
variable to store the interval ID, and create an
HTMLAudioElement object to play the sound.
If isPlaying
is true we’ll use setInterval
to play the sound and repeat it according to the bpm
.
Then, we’ll return a cleanup function that will clear the interval when the component unmounts.
We also want to update this behavior if either bpm
or isPlaying
change, so we’re going to add them to the dependency
array.
Alpine
Let’s do it now with Alpine. The functionality should be the same.
If you’ve never used Alpine it would be great if you can get a familiar before diving into this example. The Start Here section on the Alpine Docs should be more than enough.
With Alpine, you usually write everything in your HTML. However, our component is more complex than usual, so we’re
going to use the Alpine.data
method to decouple the logic from the UI.
Let’s start with the HTML.
<div x-data="metronome" x-effect="metronomeEffect">
<div>
<div><span x-text="bpm"></span> BPM</div>
<input type="range" min="60" max="240" x-model="bpm" />
</div>
<button x-on:click="isPlaying = !isPlaying" x-text="isPlaying ? 'Stop' : 'Start'"></button>
</div>
With Alpine, we use special HTML attributes called Alpine directives.
x-data
is similar to state in React. It stores all of our reactive data.x-effect
is similar touseEffect
in React. It will re-runmetronomeEffect
when a dependency changes.x-text
will set the text content of an element.x-model
connects the value of the input element to the Alpine data.x-on
allows us to run code when a DOM event is dispatched.
Now let’s take a look at the JavaScript code.
document.addEventListener("alpine:init", () => {
Alpine.data("metronome", () => {
let timer = null;
const click = new Audio("/audio/click.wav");
return {
bpm: 120,
isPlaying: false,
metronomeEffect() {
if (timer) clearInterval(timer);
if (this.isPlaying) {
timer = setInterval(() => click.play(), (60 / this.bpm) * 1000);
}
},
};
});
});
Alpine.data
allows us to register our component. The first argument is the name we’re going to use with the x-data
directive. The second argument is a function that returns the reactive data we’re going to use.
Similar to what we did with React, we’re declaring a timer
variable for storing the interval ID, creating an
HTMLAudioElement object, and setting bpm
and isPlaying
as the state of our component.
The metronomeEffect
function contains the logic of our metronome. It will run if either bpm
or isPlaying
change.
Final thoughts
React does a great job. What I like the most is that we can use custom hooks to abstract the functionality of our components. I think it’s also going to be the better option if you want to build complex components or a Single Page Application.
Alpine doesn’t stay behind. I really like its simplicity and the fact that it’s flexible enough to offer the option to decoupling our logic from the UI. I still prefer React because I’m more used to it. However, Alpine offers a simple way to add reactive behavior to websites where using libraries like React would be overkill.