DEV Community

Thomas Pikauli
Thomas Pikauli

Posted on • Updated on

Let's make a little audio player in React

Audio Player

What good is a personal website without a music player? I ask myself this, but I really don't need to. There is no better way to display what you and your homepage are all about than with a carefully chosen MP3. That's why we're going to build our own little music player.

We're not going to use any embeds. Instead we'll use HTML5 and React (Javascript). Of course we could have gone the vanilla JS route, but in the age of Gatsby websites and Create React Apps, it's fun and useful to do things within the React context.

Okay, let's go. We will build a little audio player that starts, pauses, stops and has a time indicator. Here's a working example of what functionality we'll make. Before we continue though, you will need a React app and some MP3s.

To get a React app, either use Gatsby, Create React App, a local file or CodeSandBox. If you need an MP3, you can download this track for free. Made it myself :)

Now for the code...

In order to play audio on a webpage we need the 'audio' element. This is a simple HTML tag you can place anywhere in your JSX. It's not visually rendered, so it doesn't mess up your layout. Since it is a real element, you will need to make sure it is encapsulated within another element.

<>
<h1>My Little Player</h1>
<audio />
</>
Enter fullscreen mode Exit fullscreen mode

Next up we need the MP3s. Depending on your setup you'll probably use a hardcoded link or an import. If it's an import, you reference the file and use it as a variable. The same way you would use an image. If you already have a URL that points to the MP3, that's fine too.

Initially we don't have any audio playing. But once we get a visitor to click on a song title, we want that song to play. So we need a way to log the click, identify which song was picked and then have it play.

If you're familiar with React the first two things will be straightforward. You add an 'onClick' handler to the element encapsulating the track, and add the track title as an argument in the function you feed it. A common pattern is to do all this stuff in a 'map method' and render the results in a list. This is how I did it.

class App extends React.Component {
  state = {
    selectedTrack: null
  };

  render() {
    const list = [{ id: 1, title: "Campfire Story" }, {id: 2, title: "Booting Up"}].map(item => {
      return (
        <li
          key={item.id}
          onClick={() => this.setState({ selectedTrack: item.title })}
        >
        {item.title}
        </li>
      );
    });

    return (
      <>
        <h1>My Little Player</h1>
        <ul>{list}</ul>
        <audio />
      </>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we have a list with tracks we can click. In the function we fed to the 'onClick' handler we get the track title from our array and set in the state of our app. The reason we set in our state is because we want this piece of data available to React. In that way we can show which track is currently being played anywhere in our app.

If you click now, the song won't be played yet. We first need to tell the audio element to play the track we just set in our state. But for that we need a reference to it. React has different options for this, but in our case let's go for a callback ref.

<audio ref={ref => this.player = ref} />
Enter fullscreen mode Exit fullscreen mode

The audio element is now accessible via our ref. But how and when will we access it? React has a component lifecycle that runs each time after the state or props have changed. So that means if a track is selected, this function runs. It's called 'componentDidUpdate'.

We need some logic for it to work correctly though. First we check if the state we are interested in has changed. Because 'componentDidUpdate' runs on each change of props and state in the component, we need to be careful with this. We don't want to run our code when it's not necessary. Second, we link the title to our imported file or URL. Then we access our player via our ref, we set the track, and finally call the play method.

  componentDidUpdate(prevProps, prevState) {
    if(this.state.selectedTrack !== prevState.selectedTrack) {
      let track;
      switch(this.state.selectedTrack) {
        case "Campfire Story":
          track = campfireStory
        break;
        case "Booting Up":
          track = bootingUp
        break;
        default:
        break;
      }
      if(track) {
        this.player.src = track;
        this.player.play()
      }
    }
  }
Enter fullscreen mode Exit fullscreen mode

Our track should play now on click! We're not done yet though. We need methods to pause and stop the track, and finally a duration and time indicator.

Before we build that, let's take a moment to think. In our state we currently keep track of which track is being played. That's great! But what if we're trying to add buttons for play, pause and stop to our app. It doesn't make sense to have a pause or stop button when no song is playing. Likewise a play button doesn't make sense if no song is paused.

And even if we don't care about those things, we still might want our app to know about what our music player is up to. Therefore I propose to add another piece of state.

  state = {
    selectedTrack: null,
    player: "stopped"
  };
Enter fullscreen mode Exit fullscreen mode

Next we go back to the moment we start playing our track. It's in 'componentDidUpdate', remember? When we tell the audio element to play, we now also set our 'player' state to 'playing'.

      if(track) {
        this.player.src = track;
        this.player.play()
        this.setState({player: "playing"})
      }
Enter fullscreen mode Exit fullscreen mode

Our app now knows that a song is being played. So we can use that knowledge to conditionally render a play, pause and stop button. Maybe you're thinking, the audio element itself already knows it's playing something right, why do all this? And that is correct. But a bit like we idiomatically like to give React 'control' over our input fields, it is also a good idea to do this with the audio element. It makes life much easier, and we can use widely used React patterns.

       <>
        <h1>My Little Player</h1>
        <ul>{list}</ul>
        <div>
          {this.state.player === "paused" && (
            <button onClick={() => this.setState({ player: "playing" })}>
              Play
            </button>
          )}
          {this.state.player === "playing" && (
            <button onClick={() => this.setState({ player: "paused" })}>
              Pause
            </button>
          )}
          {this.state.player === "playing" || this.state.player === "paused" ? (
            <button onClick={() => this.setState({ player: "stopped" })}>
              Stop
            </button>
          ) : (
              ""
            )}
        </div>
        <audio ref={ref => (this.player = ref)} />
      </>

Enter fullscreen mode Exit fullscreen mode

So, to make our song pause or stop. As you can see, we use an 'onClick' handler to modify the 'player' state. This doesn't stop the audio itself, but works more like a signal that we need to handle that change. Where do we do that? The 'componentDidUpdate' lifecycle!

 if (this.state.player !== prevState.player) {
      if (this.state.player === "paused") {
        this.player.pause();
      } else if (this.state.player === "stopped") {
        this.player.pause();
        this.player.currentTime = 0;
        this.setState({ selectedTrack: null });
      } else if (
        this.state.player === "playing" &&
        prevState.player === "paused"
      ) {
        this.player.play();
      }
    }
Enter fullscreen mode Exit fullscreen mode

You might notice that there is no official 'stop' method. Instead we pause the song, and set the playertime back to 0. Same difference.

We're about done. The only thing we need to do is add a duration and a time indicator. Both the duration and the current time are pieces of data that can be extracted from the audio element. To get them, we need to use a bit of a different approach though.

Unfortunately (or maybe fortunately) we can't just reference the audio element in our render method, access the 'currentTime' attribute and expect it to keep updating each second. Therefore we need a way for React to keep its own administration, so it can display the actual time based on that. We can solve this with an 'event listener'. You might know these from your Vanilla Javascript code.

Since we're working in React, we need to think about our lifecycles. The elements we want to listen to will not always be there on the page. They have a life that begins and ends (the cycle!). So when the element has appeared we add the listener, and when the element is about to be erased, we remove the listener.

Luckily, Javascript has a specific listener for updates in playing time. It's called 'timeupdate'. We use that and then save what we need to the state of our component. Maybe you've done something similar with window heights or widths. It's a common pattern.

  componentDidMount() {
    this.player.addEventListener("timeupdate", e => {
      this.setState({
        currentTime: e.target.currentTime,
        duration: e.target.duration
      });
    });
  }

  componentWillUnmount() {
    this.player.removeEventListener("timeupdate", () => {});
  }
Enter fullscreen mode Exit fullscreen mode

Now each time the song progresses, the state is updated. That means React now knows the current time and duration. We can use that to display it in our app. But before we do that, we need to format the data a bit. Here's a little helper function you can use.

function getTime(time) {
  if(!isNaN(time)) {
    return Math.floor(time / 60) + ':' + ('0' + Math.floor(time % 60)).slice(-2)
  }
}
Enter fullscreen mode Exit fullscreen mode

That just leaves us to render it on our screen.

  const currentTime = getTime(this.state.currentTime)
  const duration = getTime(this.state.duration)
Enter fullscreen mode Exit fullscreen mode
        {this.state.player === "playing" || this.state.player === "paused" ? (
          <div>
            {currentTime} / {duration}
          </div>
        ) : (
          ""
        )}
Enter fullscreen mode Exit fullscreen mode

With that final piece of information in the state we can render everything that we need for our player. The buttons, the duration, the track title, and the time indicator. Use your creativity to make something nice!

Here's the full code: https://codesandbox.io/s/5y4vjn877x

Top comments (1)

Collapse
 
brucegenerator profile image
brucegenerator

I really enjoy your tutorials but I am having trouble incorporating this component as a part of my app. When it runs it only displays the JSX elements

MusicPlayer

and the
    elements. No buttons are rendered. Please explain how I can make this work