Building a podcast app: Media playback in React

Inspiration: Achieving Media Playback

I'm currently working on a custom podcast application to display and listen to episodes for my friends over at It's Not A Book Club Podcast. The ability to manage episode playback within my code is an important part of achieving this.

The focus of this article is defining the process I used to implement this core functionality. I'm building the app using Next.js, a React framework, however the steps outlined below are applicable to any React framework.

Building Blocks

There are a few things we need to achieve our outcome. I will break down each component step by step in the following section.

Note: For readability, I have omitted code that is unimportant to the section being discussed. The example is shown in full in the code sandbox at the end of the article.

State: isPlaying

The play() and pause() methods have no impact on an element whose internal state is already registered as playing or paused, respectively. It is important that we can check this state before we choose which method to call.

isPlaying is the state variable we will use to keep track of whether the episode audio is playing or paused. Its data type is boolean.

I add this state variable at the top of my AudioPlayer function component with the useState hook.

export default function AudioPlayer({ episodes }) {
  // isPlaying is initialised to false (no playback).

  let [isPlaying, setIsPlaying] = useState(false)

  // the rest of the component code is hidden for clarity
}

There are also stylistic reasons for having this state variable, which we will discuss later.

Audio Element

The <audio> element is the intrinsic element we will use to embed sound content in our application, via its src attribute.

Note: The source of the podcast audio is out of the scope of this article, but if you are interested I'm using the rss-to-json npm package to convert an rss feed to json, and access the episode audio from there.

<audio src={episodeAudio}></audio>

A Reference to the DOM Node

The primary methods I need to control audio playback are play() and pause(). In order to access and call these methods, I need a reference to the DOM node representing the <audio> element. This reference exposes the built-in web APIs defined on this node.

In this case, the play() and pause() methods are part of the HTMLMediaElement interface of the DOM API.

Setting up the reference takes place in three steps.

  1. Importing the useRef Hook from React
import { useRef } from 'react'
  1. Declaring the ref inside our AudioPlayer component
const myRef = useRef(null)
  1. Passing it to the DOM node as a ref attribute
<audio ref={myRef} src={episodeAudio}></audio>

Event Handler

An event handler to be executed following a click event from the user. This is where we will call the play() or pause() methods using our reference to the <audio> DOM node, and update the isPlaying variable to reflect the change in state.

In order to access the DOM node methods, we must first access the .current property of our ref. From here we can access the methods specific to the node.

const handlePlay = () => {
  // accessing the play() method
  myRef.current.play()

  // updating the value of the state variable
  setIsPlaying((prevValue) => !prevValue)
}

Button Element

We will use a <button> element to listen for user click events. The onClick attribute details the expected behaviour:

If isPlaying is true, call the handlePause event handler, otherwise call the handlePlay event handler

I have also nested <svg> elements within the <button> that display play and pause icons.


  function PlayButton() {
    return (
      <button onClick={isPlaying ? handlePause : handlePlay}>

       // <svg> element displaying icon, code omitted for brevity

      </button>
    )
  }

Conditional Rendering

This is more of a stylistic concern, albeit an important one considering the layout of most media applications.

Generally, icons for play and pause buttons are shown based on the current state of the media source.

The play button is displayed when the media is paused (isPlaying == false).

The pause button is displayed when the media is playing (isPlaying == true).

We can use our isPlaying state to help with this.

I'm using a conditional (ternary) operator to evaluate the isPlaying state variable and display the PlayButton or PauseButton components based on the result.

</div>
{isPlaying ? PauseButton() : PlayButton()}
</div>

The Result

OK. That's everything we need to achieve the desired audio playback! In the context of a real application, you might choose to use a different React Hook, useReducer to manage complex interconnected state logic, like displaying the running time of the episode or managing the playback rate (1x, 2x speed etc.). I've chosen to keep things simple here with the useState hook.

This CodeSandbox shows the finished result.