Building a metronome: React vs Alpine

Updated: in 50ad7ce

React vs Alpine

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.

120 BPM

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:

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.

BPM

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.

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.


Comments