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.
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.
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.
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.
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 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.