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.
- Importing the
useRef
Hook from React
import { useRef } from 'react'
- Declaring the ref inside our
AudioPlayer
component
const myRef = useRef(null)
- 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.