Build An AudioPlayer With React

07/18/22

Intro

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.

Demo

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.

That's What It Takes
Neffex
0:00
0:00
  • That's What It Takes
    Neffex
    2:56
  • Commander Impulse
    DivKid
    3:01
  • Rooster
    Telecasted
    2:22
  • Thunder
    Telecasted
    2:37

How Our MP3 Player Works

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.

Getting Audio Files

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.

Application State

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.

Creating The Component Files

Our audio player is going to consist of three components:

  • AudioPlayer
  • Playbar
  • SongList

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

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:

react audio player 1

This should be all that we need to do for the AudioPlayer component. We can hop to the Playbar component next.

Playbar

The Playbar will contain the most functionality out of all of our components. Let's breakdown it's functionality:

  • A progress bar that displays what part of the song the user is at. It also needs to be clickable, to accomodate rewind and fast forwarding.
  • A play/pause button.
  • Previous and next buttons to skip to different songs.

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:

react audio player 2

SongList

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.

Summary

If you didn't use the audio player at the beginning of the article, here it is again.

That's What It Takes
Neffex
0:00
0:00
  • That's What It Takes
    Neffex
    2:56
  • Commander Impulse
    DivKid
    3:01
  • Rooster
    Telecasted
    2:22
  • Thunder
    Telecasted
    2:37

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.

Want To Level Up Your JavaScript Game?

Book a private session with me, and you will be writing slick JavaScript code in no time.