Sharing Audio in React with useContext
Published: 2020-04-21 20:10:41

---
title: Sharing Audio in React with useContext
published: true
date: 2020-04-21 00:00:00 UTC
tags: 
canonical_url: https://campedersen.com/2020/04/21/react-audio/
---

I ❤️ React Hooks, and my favorite lately is `useContext`.

I was working with [YKYZ](https://ykyz.com) recently, an audio-based social network. They have a feature that lets you listen to all Bleats (audio uploads) one after the other. But the mechanism was breaking.

Most browsers now disable autoplaying audio by requiring a user interaction to `play()` an `Audio` instance. I figured the bug had something to do with this, so after confirming the behavior on several browsers I started digging into the code. I noticed that every `Bleat` component had its own `Audio` instance, and the autoplay was coordinated by a higher-level component.

After reading some MDN docs, I realized that most browsers must implement the interaction component per-`Audio` rather than per-pageload, which was an assumption in the existing code. I threw together a tiny proof of concept to see if detecting the end of the clip, swapping the `src`, and playing again could get around the interaction requirement, and it did. But how to implement a shared audio context in React?

![Sharing audio like Jim and Pam](https://campedersen.com/images/jim-and-pam.jpg)

<center><small>Sharing audio like Jim and Pam</small></center>
## Context vs Props

Most times when a codebase needs to share state, people jump to [Redux](https://redux.js.org/). As a quick overview, Redux implements the Dispatcher pattern from [Flux](https://facebook.github.io/flux/docs/dispatcher) - basically just a JSON store, but funneling all the mutations to that store through a central point to ensure all those different mutations are exlicitly defined. [react-redux](https://react-redux.js.org/) is then used to pass this JSON store and dispatcher down through React Context, and `connect` is used to patch this context into the component’s props.

None of this is really necessary, especially just to share an Audio instance. Also, Redux wasn’t in the codebase I was using and this wasn’t the right reason to introduce it. All we really want to do here is decouple the Audio itself, the control of that Audio, and the display of the current state of the Audio. We can express all of that in React through components.

When [inverting control](https://en.wikipedia.org/wiki/Inversion_of_control) of the Audio, this takes the form of putting the shared `Audio` component toward the top of the tree rather than the bottom, where each Bleat had its own Audio instance. We could pass the instance down through [props](https://reactjs.org/docs/components-and-props.html) but that could end up really messy, because the components which use it might be in arbitary positions in the component tree. The solution here is [context](https://reactjs.org/docs/context.html), the same mechanism react-redux uses to share Redux’s store down to `connect`.

## Visualizing the options

We can use a simplified component hierarchy to show the differences between the context and props approaches.

Before our changes:

```
<Playlist>
  <Bleat>
    <Audio />
  </Bleat>
  <Bleat>
    <Audio />
  </Bleat>
</Playlist>

```

Using props to invert control of the audio:

```
<Audio>
  <Playlist audio={audio}>
    <Bleat audio={audio} />
    <Bleat audio={audio} />
  </Playlist>
</Audio>

```

Using context to pass the audio down without threading props:

```
<AudioProvider>
  <Playlist>
    <Bleat />
    <Bleat />
  </Playlist>
</Audio>

```

## Sharing the audio

We want to share an `Audio` instance. We could also put it into a `ref` to defer its instantiation to the component’s lifecycle, but this works for now.

```
const audio = new Audio();

```

We start by defining a `context`, and initializing it with the Audio element we just created. We could also pass null `null` so consumers of the context know if it’s been initialized or not, but we have it so we might as well use it. The `null` pattern is also common if you are loading something asynchronously that you’ll provide later.

```
const AudioContext = React.createContext(audio);

```

Now we want to define a provider component. Notice we’re exporting this - `AudioContext.Provider` could just go straight into the app, but breaking it out allows us to package this nicely into a file without exporting the `AudioContext` we created above.

It also allows you do define more complex hooks in your `Provider` and pass them down, but we don’t need that here. Passing `audio` here could instead be a return value of `useRef` or `useState` rather than just the constant above.

```
export const AudioProvider = ({ children }) => (
  <AudioContext.Provider value={audio}>{children}</AudioContext.Provider>
);

```

Now how do we get access to the `audio` further down the tree? First we need to wrap any components that want to use it with the Provider we created:

```
const App = () => (
  <AudioProvider>
    <Playlist />
  </AudioProvider>
);

```

We can wrap the `useContext` hook, again to avoid exporting the AudioContext:

```
export const useAudio = React.useContext(AudioContext);

```

Now we can use our custom hook down the tree with `useAudio`:

```
const Playlist = () => {
  const audio = useAudio();

  const play = React.useCallback((src) => {
    audio.src = src;
    audio.play();
  }, [audio]);

  ...
};

```

## Making a playlist

Now that we have a shared audio context, let’s make it do something. We’ll centralize control into the `Playlist` component. It will hold info about all the clips to play (in a real application this would come from the server, but we’re just using a constant for now). Let’s add some state to the Playlist:

```
const [isPlaying, setIsPlaying] = React.useState(false);
const [currentIndex, setCurrentIndex] = React.useState(null);
const [isPlayingAll, setIsPlayingAll] = React.useState(false);

```

When the clip ends, let’s move to the next one. We create this event listener inside a `useEffect` to allow cleanup when the component unmounts:

```
const maybeAdvancePlaylist = React.useCallback(() => {
  const newIndex = currentIndex + 1;

  // stop at the last clip
  if (newIndex > BLEATS.length) {
    return;
  }

  // don't advance if a single clip was played
  if (isPlayingAll) {
    setCurrentIndex(newIndex);
    audio.src = BLEATS[newIndex].src;
    audio.play();
  }
}, [currentIndex, isPlayingAll]);

React.useEffect(() => {
  audio.addEventListener("ended", maybeAdvancePlaylist);

  // when unmounting, clean up the event listener we've added
  return () => {
    audio.removeEventListener("ended", maybeAdvancePlaylist);
  };
}, [audio, maybeAdvancePlaylist]);

```

## Let’s see it!

The following audio files should play through, using a shared audio context:

[View this post on campedersen.com for a working example](https://campedersen.com/2020/04/21/react-audio/)

## Hooks are fun

The context + hooks approach has several advantages:

1. it doesn’t use any external libraries, but you could easily share [Howler](https://howlerjs.com/) this way, for instance
2. you can bring the `AudioContext` file over to another project easily without altering its Redux system
3. we can provide this functionality surgically, only touching the components we need, by not threading props
4. the changes to the codebase are obvious - `useAudio` can’t be much clearer in my opinion

But the best part is that it’s **fun**! Hooks are super simple to write, refactor, and abstract. And in my opinion frontend work is fun because you get to interact with it - audio adds on top of the visual part of that!

### Thanks

Thanks to [Dave](https://davekiss.com/) and [Dylan](https://dylanjpierce.com/) for helping proof read this post (my first in a couple years!) and to [YKYZ](https://ykyz.com) for allowing me to write about this work.