Today I will be showing you how to create an audio player component with React. It will consist of three separate components that when combined, will make for the final functionality desired. I don't recommend coding along with this one, and instead, recommend trying to understand what's going on at a high level. After reading this article you will have a very good sense of how you could build something like this yourself.
On top of being able to use the audio player at the end of the article, I will also include a link to a repository where
I've recreated the audio player with create-react-app
.
Instead of adding a screenshot, I'd figure it would be better to allow you to use the audio player, so you can get a better sense of how it works before diving into the article.
In order to make our audio player work seamlessly between different components, we are going to use React's context
feature.
This is similar to Redux, where it allows you to maintain a global
state. We will utilize a reducer, and pass it's state down via context,
so that all components can access whatever state they'd like.
For this article, I downloaded four different songs from the YouTube Free Audio Library.
Since my website utilizes Next.js, I then created a folder inside of my public
directory named audio
, and placed those four files inside of it.
Now that we have our physical audio files, I've come up with what our application state needs to look like.
import songs from './songs.json';
export const initialState = {
playing: false,
currentTime: 0,
duration: 0,
songIndex: 0,
songs,
};
export default function reducer(state, action) {
switch (action.type) {
case 'SET_PLAYING':
return { ...state, playing: action.payload };
case 'CHOOSE_SONG':
return {
...state,
playing: true,
songIndex: action.payload,
};
case 'SET_DURATION':
return { ...state, duration: action.payload };
case 'SET_CURRENT_TIME':
return { ...state, currentTime: action.payload };
case 'NEXT_SONG':
return {
...state,
songIndex:
state.songIndex === songs.length - 1 ? 0 : state.songIndex + 1,
playing: true,
};
case 'PREV_SONG':
return {
...state,
songIndex:
state.songIndex === 0 ? state.songs.length - 1 : state.songIndex - 1,
playing: true,
};
default:
return state;
}
}
Don't worry too much about all of the cases in the reducer, they will make more sense when we start building the components.
Also, If you are curious as to what songs.json
looks like, it is a basic array of objects.
[
{
"title": "That's What It Takes",
"artist": "Neffex",
"duration": "2:56"
},
{
"title": "Commander Impulse",
"artist": "DivKid",
"duration": "3:01"
},
{
"title": "Rooster",
"artist": "Telecasted",
"duration": "2:22"
},
{
"title": "Thunder",
"artist": "Telecasted",
"duration": "2:37"
}
]
These correspond to the audio files that I placed in public/audio
. Again, this will make more sense later on.
Our audio player is going to consist of three components:
Inside of a components
folder I am going to create an AudioPlayer
directory, and then create three .js
files
with the names listed above. Let's dive into AudioPlayer
first, as it is the top level parent component.
AudioPlayer will be responsible for handling our state, as well as being a container for Playbar
and SongList
.
Our AudioPlayer
component will look like this:
import { createContext, useReducer } from 'react';
import reducer, { initialState } from './reducer';
import { css } from '@emotion/react';
import Playbar from './Playbar';
import SongList from './SongList';
export const StoreContext = createContext(null);
export default function AudioPlayer() {
const [state, dispatch] = useReducer(reducer, initialState);
const song = state.songs[state.songIndex];
return (
<StoreContext.Provider value={{ state, dispatch }}>
<div css={CSS}>
<div className="title">{song.title}</div>
<div className="artist-info">{song.artist}</div>
<Playbar />
<SongList />
</div>
</StoreContext.Provider>
);
}
const CSS = css`
background: #1b1c1e;
width: 100%;
margin: 0 auto;
margin-top: 60px;
padding: 20px;
.title {
color: white;
font-weight: bold;
}
.artist-info {
color: #777;
}
`;
With this code in place, this is what should be visible in the browser:
This should be all that we need to do for the AudioPlayer
component. We can hop to the Playbar
component next.
The Playbar will contain the most functionality out of all of our components. Let's breakdown it's functionality:
Let's layout the entirety of Playbar
.
import { useRef, useContext, useEffect } from 'react';
import { css } from '@emotion/react';
import { StoreContext } from './AudioPlayer';
const formatTime = (inputSeconds) => {
let seconds = Math.floor(inputSeconds % 60);
if (seconds < 10) seconds = `0${seconds}`;
const minutes = Math.floor(inputSeconds / 60);
return `${minutes}:${seconds}`;
};
const handleProgress = (currentTime, duration) => {
if (currentTime === 0 && duration === 0) {
return 0;
}
return 100 * (currentTime / duration);
};
export default function Playbar() {
const { state, dispatch } = useContext(StoreContext);
const audioRef = useRef();
const { playing, songIndex, songs, currentTime, duration } = state;
useEffect(() => {
if (playing) {
audioRef.current.play();
} else {
audioRef.current.pause();
}
}, [playing, songIndex]);
// This is used on initial mount to set the duration of the first song.
// It is not activated after initial mount.
useEffect(() => {
if (!audioRef.current) {
return;
}
dispatch({
type: 'SET_DURATION',
payload: audioRef.current.duration,
});
}, [dispatch]);
return (
<div className="playbar">
<audio
ref={audioRef}
src={`/audio/${songs[songIndex].title}.mp3`}
onLoadedMetadata={() =>
dispatch({
type: 'SET_DURATION',
payload: audioRef.current.duration,
})
}
onTimeUpdate={(e) =>
dispatch({ type: 'SET_CURRENT_TIME', payload: e.target.currentTime })
}
onEnded={() =>
dispatch({
type: 'NEXT_SONG',
})
}
/>
<div className="time">
<div>{formatTime(Math.floor(currentTime))}</div>
<div>{formatTime(duration)}</div>
</div>
<div
className="progress"
onClick={(e) => {
const { offsetLeft, offsetParent, offsetWidth } = e.target;
const pos =
(e.pageX - (offsetLeft + offsetParent.offsetLeft)) / offsetWidth;
audioRef.current.currentTime = pos * audioRef.current.duration;
}}
>
<div
className="progress-inner"
style={{ width: `${handleProgress(currentTime, duration)}%` }}
/>
</div>
<div className="controls">
<button
onClick={() => {
dispatch({
type: 'PREV_SONG',
});
}}
>
<i className="fa fa-backward" />
</button>
<button
onClick={() => {
dispatch({
type: 'SET_PLAYING',
payload: playing ? false : true,
});
}}
>
{playing ? (
<i className="fa fa-pause" />
) : (
<i className="fa fa-play" />
)}
</button>
<button
onClick={() => {
dispatch({
type: 'NEXT_SONG',
});
}}
>
<i className="fa fa-forward" />
</button>
</div>
</div>
);
}
const CSS = css`
margin-top: 40px;
position: relative;
.progress {
position: relative;
width: 100%;
height: 5px;
border-radius: 20px;
background: #5f6061;
cursor: pointer;
}
.progress-inner {
height: 100%;
background: #ff4734;
border-radius: 20px;
pointer-events: none;
}
.time {
display: flex;
justify-content: space-between;
color: #777;
margin-bottom: 7.5px;
font-size: 14px;
}
.controls {
margin-top: 20px;
}
.controls > button {
background: none;
padding: 0;
border: none;
color: white;
margin-right: 20px;
cursor: pointer;
outline: 0;
}
.controls > button > i.fa-backward,
.controls > button > i.fa-forward {
font-size: 16px;
}
.controls > button > i.fa-play,
.controls > button > i.fa-pause {
font-size: 20px;
}
`;
With this code in place, the Playbar
should look like this:
Now all we need to do is display the list of songs below the playbar. Let's create the component for that now.
import { useContext } from 'react';
import { css } from '@emotion/react';
import { StoreContext } from './AudioPlayer';
export default function SongList() {
const { state, dispatch } = useContext(StoreContext);
const { songIndex, songs } = state;
return (
<div css={CSS}>
<ul>
{songs.map((song, i) => (
<li
key={song.title}
onClick={() => {
dispatch({ type: 'CHOOSE_SONG', payload: i });
}}
>
<div>
<div
className="title"
style={{
color: songIndex === i ? '#FF4734' : 'white',
}}
>
{song.title}
</div>
<div className="artist">{song.artist}</div>
</div>
<div className="duration">{song.duration}</div>
</li>
))}
</ul>
</div>
);
}
const CSS = css`
margin-top: 25px;
li {
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
}
li:not(:last-of-type) {
margin-bottom: 10px;
}
li .title {
color: white;
font-weight: normal;
}
li .artist,
li .duration {
color: #777;
}
`;
With this in place, we are basically finished. Instead of showing you another screenshot, I think it's time we can take the audio player for a spin.
If you didn't use the audio player at the beginning of the article, here it is again.
That's about it for our audio player. As always, if you have any problems understanding any of this, feel free to shoot me a message, and I'll make sure it's crystal clear.
P.S. Here is a full working version on Github.