ReactT2 Dark_1200x303
Learn how to create a React audio library using the HTMLAudioElement API and use it in your projects.

Browsers are evolving and launching new APIs every year, helping us to build more reliable and consistent applications on the web. A few years ago, working with audio on the web was a pretty difficult job. There were no good APIs available and browsers offered poor support.

The struggle to work with audio is still real, especially in React applications. There are not too many good and reliable audio libraries to work with in React—most add difficulties to our work and don’t function how we want to use audio in our applications.

We have a few options to work with audio in JavaScript applications. The most known and used is Howler.js. The only problem with Howler.js is that it does not have a React wrapper available, so the idea of integrating it in your React application can get harder and unsustainable along the way, and some unexpected bugs and errors might happen.

A solution we can use to work with audio in React applications is to create our own React audio library using a native API. So, let’s learn more about how the HTMLAudioElement API works, and then we’ll start to create our own React audio library, so we can easily work with audio in our React applications.

HTMLAudioElement

The HTMLAudioElement API is an interface that provides a way to access the properties of an <audio> element.

It has a constructor called Audio() that receives a URL string and returns a new HTMLAudioElement object.

const audioElement = new Audio(src);

The HTMLAudioElement does not have any properties, but it inherits properties from the HTMLMediaElement interface.

The HTMLMediaElement interface has a variety of different methods and properties that we can work with to create something really useful. For example, to play audio after creating a new instance using the Audio() constructor, all we have to do is use the play() method.

audioElement.play();

We can pause the audio using the pause() method.

audioElement.pause();

The HTMLMediaElement interface also has a lot of different events that we can work with, for example, the loadeddata event. The loadeddata event is fired after the audio has finished loading.

audioElement.addEventListener('loadeddata', (event) => {
 console.log('Finished loading!');
});

The HTMLAudioElement API helps us to work with audio by providing a lot of different properties, methods and events. Let’s start to create our own React audio library using it and see what will be the final result.

Getting Started

Starting the setup of a new library is sometimes a pain and requires a lot of time. That’s why we will use TSDX.

TSDX is a zero-config CLI that helps us to create a new React library with ease, without us having to set up anything more. We will also use npx, which is a CLI that helps us to easily install and manage dependencies hosted in the npm registry.

Let’s start the process of creation of our React audio library using TSDX. You can give it any name you want. In your console, give the following command:

npx tsdx create audio-library

TSDX gives a nice structure to start with our library. Inside our src folder, we have our index.tsx file, and we can delete everything that’s inside this file.

Inside our index.tsx file, we will put the following:

export { default as useAudio } from './useAudio';

We will export the useAudio file only. Inside our src folder, let’s create our useAudio.ts file. This file will be a custom React hook, so let’s import some built-in hooks from React and create our function.

import { useState, useCallback, useEffect, useRef } from "react";
const useAudio = () => {
  ...
} 
export default useAudio;

TSDX provides by default a TypeScript config, so let’s make use of it. We will create an interface called UseAudioArgs, which will be the arguments that our useAudio hook can receive, and then pass it to our useAudio custom hook.

import { useState, useCallback, useEffect, useRef } from "react";
interface UseAudioArgs {
 src: string;
 preload?: boolean;
 autoplay?: boolean;
 volume?: number;
 mute?: boolean;
 loop?: boolean;
 rate?: number;
}
const useAudio= ({
 src,
 preload = true,
 autoplay = false,
 volume = 0.5,
 mute = false,
 loop = false,
 rate = 1.0,
}: UseAudioArgs) => {
 ...
}
export default useAudio;

Now, inside our hook, let’s make use of the useState, a built-in hook from React to manage our state.

const [audio, setAudio] = useState<HTMLAudioElement | undefined>(undefined);
const [audioReady, setAudioReady] = useState<boolean>(false);
const [audioLoading, setAudioLoading] = useState<boolean>(true);
const [audioError, setAudioError] = useState<string>("")
const [audioPlaying, setAudioPlaying] = useState<boolean>(false);
const [audioPaused, setAudioPaused] = useState<boolean>(false);
const [audioDuration, setAudioDuration] = useState<number>(0);
const [audioMute, setAudioMute] = useState<boolean>(false);
const [audioLoop, setAudioLoop] = useState<boolean>(false);
const [audioVolume, setAudioVolume] = useState<number>(volume);
const [audioSeek, setAudioSeek] = useState<number>(rate);
const [audioRate, setAudioRate] = useState<number>(0);

We will have a few different states. The audioLoading will be true by default, and we will set it to false once the audio is loaded. After the audio is loaded and the audioLoading is false, we will set the audioReady to true, so we can identify when the audio is ready to play. In case any error occurs, we will use the audioError state.

Let’s also create a ref called audioSeekRef, which we will use in the future to update our audioSeek state.

const audioSeekRef = useRef<number>();

Now let’s create a function called newAudio. Inside this function, we will create a new HTMLAudioElement object using the Audio() constructor. Also, we will set some properties of our new HTMLAudioElement object using the arguments that we received in our useAudio hook.

This is how our newAudio function will look:

const newAudio = useCallback(
 ({
   src,
   autoplay = false,
   volume = 0.5,
   mute = false,
   loop = false,
   rate = 1.0,
 }): HTMLAudioElement => {
   const audioElement = new Audio(src);
   audioElement.autoplay = autoplay;
   audioElement.volume = volume;
   audioElement.muted = mute;
   audioElement.loop = loop;
   audioElement.playbackRate = rate;
   return audioElement;
 },
[]);

Next, we will create a function called load. This function will be responsible to load our audio source and change our state by listening to the events. Inside our load function, we will call our newAudio function to create a new HTMLAudioElement object.

const load = useCallback(
 ({ src, preload, autoplay, volume, mute, loop, rate }) => {
   const newAudioElement = newAudio({
     src,
     preload,
     autoplay,
     volume,
     mute,
     loop,
     rate,
   });
 },
[newAudio]);

Inside our load function, the first events that we will listen to are the abort and error events. In case any error occurs, we will set our audioError state to an error message.

newAudioElement.addEventListener('abort', () => setAudioError("Error!"));
newAudioElement.addEventListener('error', () => setAudioError("Error!"));

Now, we will listen to the loadeddata event, which will be fired when the audio is ready to be played. Inside our callback function, we will check if the autoplay argument is true. If it is, the audio will autoplay by default.

newAudioElement.addEventListener('loadeddata', () => {
 if (autoplay) {
   setAudioLoading(false);
   setAudioReady(true);
   setAudioDuration(newAudioElement.duration);
   setAudioMute(mute);
   setAudioLoop(loop)
   setAudioPlaying(true);
 } else {
   setAudioLoading(false);
   setAudioReady(true);
   setAudioDuration(newAudioElement.duration);
   setAudioMute(mute);
   setAudioLoop(loop);
 }
});

Now the play and pause events. Every time the audio is set to play, we will set our audioPlaying state to true and our audioPaused to false. We will do the same but in an inverted way for the pause event.

newAudioElement.addEventListener('play', () => {
 setAudioPlaying(true);
 setAudioPaused(false);
});
newAudioElement.addEventListener('pause', () => {
 setAudioPlaying(false);
 setAudioPaused(true);
});

The last event that we will listen to is the ended event. Inside the callback function of this event, when the audio has ended, we will set all of our states to the default state.

newAudioElement.addEventListener('ended', () => {
 setAudioPlaying(false);
 setAudioPaused(false);
 setAudioSeek(0);
 setAudioLoading(false);
 setAudioError("");
});

Now, at the end of our load function, we will set our audio and pass the newAudiofunction as a callback dependency. If you followed all the steps until here, this is how our load function will look:

const load = useCallback(({ src, preload, autoplay, volume, mute, loop, rate }) => {
 const newAudioElement = newAudio({
   src,
   preload,
   autoplay,
   volume,
   mute,
   loop,
   rate,
 });
newAudioElement.addEventListener('abort', () => setAudioError("Error!"));
newAudioElement.addEventListener('error', () => setAudioError("Error!"));
newAudioElement.addEventListener('loadeddata', () => {
 if (autoplay) {
   setAudioLoading(false);
   setAudioReady(true);
   setAudioDuration(newAudioElement.duration);
   setAudioMute(mute);
   setAudioLoop(loop)
   setAudioPlaying(true);
 } else {
   setAudioLoading(false);
   setAudioReady(true);
   setAudioDuration(newAudioElement.duration);
   setAudioMute(mute);
   setAudioLoop(loop);
 }
});
newAudioElement.addEventListener('play', () => {
 setAudioPlaying(true);
 setAudioPaused(false);
});
newAudioElement.addEventListener('pause', () => {
 setAudioPlaying(false);
 setAudioPaused(true);
});
newAudioElement.addEventListener('ended', () => {
 setAudioPlaying(false);
 setAudioPaused(false);
 setAudioSeek(0);
 setAudioLoading(false);
 setAudioError("");
});
setAudio(newAudioElement);
},
[newAudio]
);

Now, after creating our load function, let’s use the useEffect hook to load our audio.

useEffect(() => {
 if (!src) return;
 if (!preload) return;
 load({ src, autoplay, volume, mute, loop, rate });
}, [src, preload, autoplay, volume, mute, loop, rate, load]);

We now have the most difficult part of our audio library ready. We created the newAudio function to create a new HTMLAudioElement object and the load function to load our audio. Now it’s time to create the functions that we are going to export in our hook, so we can control our audio easily.

We will be starting by creating a function called onToggle. This function will simply play the audio, or pause it if the audio is already playing.

const onToggle = () => {
 if (!audio) return;
 if (audioReady) audio.play();
 if (audioPlaying) audio.pause();
};

Next, we will create the onPlay and onPause functions.

const onPlay = () => {
 if (!audio) return;
 audio.play();
};
const onPause = () => {
 if (!audio) return;
 audio.pause();
};

We will also create a function called onMute to mute our audio and another function called onLoop to loop the audio.

const onMute = () => {
 if (!audio) return;
 audio.muted = !audioMute;
 setAudioMute(!audioMute);
};
const onLoop = () => {
 if (!audio) return;
 audio.loop = !audioLoop;
 setAudioLoop(!audioLoop);
};

Now, we will create the final functions that will be the onVolume to change our volume, onRate to change the playback rate of our audio, and onSeek to change the current seek.

const onVolume = (e: React.ChangeEvent<HTMLInputElement>) => {
 if (!audio) return;
 const volume = parseFloat(e.target.value);
 setAudioVolume(volume);
 audio.volume = volume;
};
const onRate = (e: React.ChangeEvent<HTMLInputElement>) => {
 if (!audio) return;
 const rate = parseFloat(e.target.value);
 setAudioRate(rate);
 audio.playbackRate = rate;
};
const onSeek = (e: React.ChangeEvent<HTMLInputElement>) => {
 if (!audio) return;
 const seek = parseFloat(e.target.value);
 setAudioSeek(seek);
 audio.currentTime = seek;
};

Before we finish working on our useAudio hook, we cannot forget to use the useEffect hook again to update our audioSeek smoothly using the requestAnimationFrame API.

useEffect(() => {
 const animate = () => {
   const seek = audio?.currentTime;
   setAudioSeek(seek as number);
   audioSeekRef.current = requestAnimationFrame(animate);
 };
 if (audio && audioPlaying) {
   audioSeekRef.current = requestAnimationFrame(animate);
 }
 return () => {
   if (audioSeekRef.current) {
     window.cancelAnimationFrame(audioSeekRef.current);
   }
 };
}, [audio, audioPlaying, audioPaused]);

Now, at the end of our useAudio hook, let’s return the state and functions that we’re going to need in our audio library.

return {
 ready: audioReady,
 loading: audioLoading,
 error: audioError,
 playing: audioPlaying,
 paused: audioPaused,
 duration: audioDuration,
 mute: audioMute,
 loop: audioLoop,
 volume: audioVolume,
 seek: audioSeek,
 rate: audioRate,
 onToggle,
 onPlay,
 onPause,
 onMute,
 onLoop,
 onVolume,
 onRate,
 onSeek,
}

We’re now ready to test and see if everything is working fine. TSDX provides a folder called “Example”, so we can easily import our useAudio hook and test it.

Usage

Inside our example folder, let’s import our useAudio hook and start to play around and use it as a real example.

import { useAudio } from "../src"

We will pass and use simple audio with our useAudio hook, and set a few default arguments.

const {
 ready,
 loading,
 error,
 playing,
 paused,
 duration,
 mute,
 loop,
 volume,
 seek,
 rate,
 onToggle,
 onPlay,
 onPause,
 onMute,
 onLoop,
 onVolume,
 onRate,
 onSeek,
} = useAudio({
 src,
 preload: true,
 autoplay: false,
 volume: 0.5,
 mute: false,
 loop: false,
 rate: 1.0,
});

Now, inside our component, we’re going to create a few buttons to play and pause our audio.

return (
 <div>
   <button onClick={onToggle}>Toggle</button>
   <button onClick={onPlay}>Play</button>
   <button onClick={onPause}>Pause</button>
 </div>
);

Also, let’s create a few range inputs for our seek, rate and volume properties.

return (
 <div>
   <button onClick={onToggle}>Toggle</button>
   <button onClick={onPlay}>Play</button>
   <button onClick={onPause}>Pause</button>
  
   <div>
     <label>Seek: </label>
     <input
       type="range"
       min={0}
       max={duration}
       value={seek}
       step={0.1}
       onChange={onSeek}
     />
   </div>
   <div>
     <label>Volume: </label>
     <input
       type="range"
       min={0}
       max={1}
       value={volume}
       step={0.1}
       onChange={onVolume}
     />
   </div>
   <div>
     <label>Rate: </label>
     <input
       type="range"
       min={0.25}
       max={5.0}
       value={rate}
       step={0.1}
       onChange={onRate}
     />
   </div>
 </div>
);

We now have our React audio library working pretty well. There’s a lot more we could do and implement in this library, like making use of the Context API so we can use our audio state logic in different components in our React tree, for example.

The HTMLAudioElement API is pretty powerful and simple to work with, and it allows us to create some incredible applications using audio on the web. In case you need something more sophisticated to work with audio, you can use the Web Audio API, which is similar but way more powerful and versatile to work with audio. You can use a few things like audio effects, sprites, audio visualizations and much more.

Conclusion

In this article, we learned about the HTMLAudioElement and created a React audio library using this powerful API. We used a few built-in React hooks for it and also created our own custom React hook, having a final result of a nice, simple and working React audio library ready for production that can be used in different ways.


Leonardo Maldonado
About the Author

Leonardo Maldonado

Leonardo is a full-stack developer, working with everything React-related, and loves to write about React and GraphQL to help developers. He also created the 33 JavaScript Concepts.

Related Posts

Comments

Comments are disabled in preview mode.