Writing The Moth Audio Player

Written by Christian de Botton, Technical Director

The Moth brings people together over the age-old art of storytelling through live performances, radio broadcasts, podcasts, and stories shared online. With all of their rich media to absorb, we at Brooklyn United wanted to create a web platform that allows users to listen to their favorite stories, as well as easily discover new content. The goal was to create an experience more like other social listening platforms, where ease of consumption and content discovery are important. Over the past few months, we built an easy-to-use discovery and consumption platform between a rebranded website and a single-page-application (SPA) audio platform that streams content from the same sources as the website.

Play, pause, queue: The Moth’s audio player in action

As the developer responsible for programming the audio player, I thought it might be worthwhile to discuss some of the considerations I took while writing the software, and why I made the decisions that I had. Obviously, I’m not able to share code samples, so let’s just talk through some thought processes.

Scalability and longterm ease of maintenance are always goals of mine when I write software. To be totally transparent, I built the app twice. The first time, I used Neo4j, a popular graph database, as a data store, and Facebook’s Relay with React as a front-end framework. The stack of tooling was hyper-efficient thanks to the sophisticated caching mechanisms of Relay, and all around really impressive. Unfortunately, it was also a mess:

  1. I didn’t have the experience to properly organize my GraphQL node map.
  2. I sort of understood Relay, integration with the the rest of the site would have been an absolute nightmare.
  3. Very importantly, Relay was an alpha release at the time, so maintenance over time would have been very painful.

What is it about React and Redux that make it so easy to write maintainable software? Functional programming. It encourages developers to write work in very incremental and testable chunks of logic. There were other important pieces to this puzzle aside from those two libraries, because again, we’re focused on longevity of code, which is a really hard thing to achieve. Let’s take a look at all of these parts and see how they play into creating a maintainable code base.


The tools used to build The Moth Audio Player included React, Redux, Reselect, Immutable.js, and Flow. Instead of looking at them separately, it’s more interesting to look at how it all works together, with the exception of Flow, which for me is really the crux of writing code that is easily understood and simple to work with.

For those who are unfamiliar with Flow, it introduces static type checking to JavaScript, much like Microsoft’s TypeScript, but it works with Babel rather than Microsoft’s own flavor of ES2015-like syntax. How does this help us? Almost all runtime errors in JavaScript occur when a function receives a parameter that is the wrong datatype. Let’s look at a very simple example:

function addFive(num) {
return num + 5;
}
const fourPlusFive = addFive(4); // 9
const arrayPlusFive = addFive([4]); // 45
const boolPlusFive = addFive(true); // 6

You can see, the results are unexpected when we try to add non-numeric variables to five. How does static typing help us? Let’s take a look:

/* @flow */
function addFive(num: number) {
return num + 5;
}
const fourPlusFive = addFive(4);
const arrayPlusFive = addFive([4]); // error
const boolPlusFive = addFive(true); // error

By adding a single type hint to the parameter of our method, Flow will now throw an error if we try to break that rule. It’s worth pointing out that this will work across files, which is where the benefit really shines, especially when using Nuclide. The IDE will tell us what the function expects as it’s used throughout the code, and also will immediately warn the developer of when they are breaking their own rules. Not only does this make code more easily understandable, but it reduces bugs significantly. For these reasons, all of the React and Redux code for The Moth’s audio player is statically typed with very strict rules.

Next I’d like to discuss using Redux and Immutable together. One of the great benefits of Redux is that it forces you to describe your state structure, making it easy to reason about what’s happening in your application. Immutable allows you to take this a step further even, by using its record class. A record is a constructor that creates a map-like object, except it has the benefit of allowing you to define default values as well as prevent new keys from being set on it once it’s been defined.

/* @flow */
import { Record, OrderedMap } from ‘immutable’;
type Track = {
id: number;
title: string;
storytellerId: number;
duration: number;
};
type TrackAction = {
type: ‘GET_TRACKS_REQUEST’ |
‘GET_TRACKS_SUCCESS’ |
‘GET_TRACKS_FAILURE’ |
‘FAVORITE_TRACK_ID’;
};
const TrackState = new Record({
loading: false,
error: null,
entities: new OrderedMap<number, Track>()
});
export default function trackReducer(
state: TrackState = new TrackState(),
action: TrackAction
): TrackState {
switch (action.type) {
default:
return state;
}
}

You can see that by using Flow and Immutable in our Redux store, we’ve precisely defined what our data can look like, and if we break any of the rules that we’ve defined, we’ll see errors as a result well before we even attempt to look at our project in a browser.

Lastly, I’d like to discuss Reselect and how it was used in the application. One of the big challenges with React applications that deal with rendering large sets of data, like The Moth’s ever growing list of stories that listeners can consume through the media player, is optimizing performance and preventing the application from constantly cascading renders. The typical approach is to use the shouldComponentUpdate lifecycle method that exists in all React components. What Reselect does is it creates selectors that can pull out data from your state, and will only signal an update if the parameters that it’s passed from the state change.

import { createSelector } from ‘reselect’;
export const getTracks = createSelector(
state => state.trackReducer.loading,
state => state.trackReducer.entities,
(loading, tracks) => {
return {
loading,
tracks: tracks.toJS(),
};
};
)

Here, I’ve created a selector that pulls out whether or not the application is currently loading new tracks, so that we can render some sort of loading state, and then it returns a vanilla Javascript object that represents our track list, converted from our previously declared Immutable Record. How do we connect this to our component? I’m hoping if you’re familiar with Redux you can probably guess, but here it is:

/* @flow */
import React, { Component } from ‘react’;
import { connect } from ‘react-redux’;
import type { Element as ReactElement } from ‘react’;
import { getTracks } from ‘../selectors’;
type Track = {
id: number;
title: string;
storytellerId: number;
duration: number;
};
type P = {
loading: boolean;
tracks: Array<Track>;
};
class TrackList extends Component<void, P, void> {
render(): ReactElement {
return (
<div>
{this.props.loading && <span>Loading…</span>}
<ul>
{this.props.tracks.map(track => (
<li key={this.props.track.id}>
{this.props.track.title}
</li>
))}
</ul>
</div>
);
}
}
export default connect(getTracks)(TrackList);

Simply connect the Reselect selector like you would your mapStateToProps method in Redux. Now the TrackList component will only receive new props if those state records that we’re concerned with are affected in any sort of state change. I won’t discuss much more on it here, but Reselect is also excellent because selectors are composable, meaning you can use selectors within selectors. If you are using redux-thunk, you can also use your selectors in actions to pull out parts of the state that you are concerned with, making the code much more DRY. This was used extensively in The Moth’s audio player.

Despite being excited to work with a graph database and use Relay, I knew that this was an application that would need to stand the test of time, and for that reason I opted to go with the toolset that I felt most comfortable with delivering a stable, scalable, and portable codebase.