» Make Pomodoro Web App in React » 2. Development » 2.7 Add Sounds

Add Sounds

If you prefer to use less third-party packages, you may try to play with an audio tag.

As shown in the following example, you can easily control an audio tag with a react Ref.

import React, { useRef } from 'react';

const SoundPlayer = () => {
  const audioRef = useRef(null);

  const playSound = () => {
    audioRef.current.play();
  };

  return (
    <div>
      <button onClick={playSound}>Play Sound</button>
      <audio ref={audioRef}>
        <source src="your-sound-file.mp3" type="audio/mp3" />
        Your browser does not support the audio tag.
      </audio>
    </div>
  );
};

export default SoundPlayer;

If you have a lot of audio files to play in a project, you'd better try howler.

HOWLER.JS

Changes in App.js:

@@ -2,11 +2,19 @@ import { useEffect, useRef, useState } from "react";
 import "./App.css";
 import Settings from "./components/Settings";
 import Timer from "./components/Timer";
+import { Howl, Howler } from "howler";
 
 const POMODORO_SECONDS = 25 * 60;
 const BREAK_SECONDS = 5 * 60;
 const PHASE_POMODORO = 0;
 const PHASE_BREAK = 1;
+
+// Sounds from https://pixabay.com/sound-effects/search/tick-tock/
+const SOUNDS = {
+  tick: process.env.PUBLIC_URL + "/tick.mp3",
+  alarm: process.env.PUBLIC_URL + "/alarm.mp3",
+  button: process.env.PUBLIC_URL + "/button.mp3",
+};
 const DEFAULT_SETTING = {
   useCircle: true,
   soundOn: true,
@@ -22,12 +30,29 @@ function App() {
   useEffect(() => {
     if (seconds === 0) {
       stopTimer();
-      alarm();
+      // Hacking for Howler.stop() method
+      setTimeout(() => {
+        // alarm
+        playShortSound(SOUNDS.alarm);
+      }, 10);
     }
   }, [seconds]);
 
+  useEffect(() => {
+    if (ticking) {
+      playLoopSound(SOUNDS.tick);
+    } else {
+      stopSound(tickSoundIdRef.current);
+    }
+  }, [ticking]);
+
+  useEffect(() => {
+    Howler.mute(!settings.soundOn);
+  }, [settings.soundOn]);
+
   // use the `useRef` hook to create a mutable object that persists across renders
   const intervalIdRef = useRef(null);
+  const tickSoundIdRef = useRef(null);
 
   const startTimer = () => {
     setTicking(true);
@@ -49,6 +74,7 @@ function App() {
   };
 
   const toggleTimer = () => {
+    playShortSound(SOUNDS.button);
     if (ticking) {
       // Clicked "Pause"
       stopTimer();
@@ -69,6 +95,11 @@ function App() {
     return seconds / duration;
   };
 
+  const skippable = () => {
+    const percentage = calcPercentage();
+    return percentage < 1 && percentage > 0;
+  };
+
   const pickPhase = (phase) => {
     const secBg = "secondary-bg";
     if (phase === PHASE_POMODORO) {
@@ -81,13 +112,30 @@ function App() {
   };
 
   const skipPhase = () => {
+    playShortSound(SOUNDS.button);
     const newPhase = (phase + 1) % 2;
     pickPhase(newPhase);
   };
 
-  const alarm = () => {
-    // TODO: play some sound
-    console.log("Time's up!");
+  const playLoopSound = (url) => {
+    const sound = new Howl({
+      src: [url],
+      loop: true,
+    });
+    tickSoundIdRef.current = sound.play();
+  };
+
+  const playShortSound = (url) => {
+    const sound = new Howl({
+      src: [url],
+    });
+    sound.play();
+  };
+
+  const stopSound = (soundId) => {
+    if (soundId) {
+      Howler.stop(soundId);
+    }
   };
 
   return (
@@ -131,7 +179,7 @@ function App() {
             {ticking ? "Pause" : seconds === 0 ? "Next" : "Start"}
           </button>
         </div>
-        <span className="skip-btn" onClick={skipPhase}>
+        <span hidden={!skippable()} className="skip-btn" onClick={skipPhase}>
           skip
         </span>
       </div>