Build a Spotify clone with React and ts-audio
Tags: react, webdev
Published: 2022-06-01 15:37:00

**Written by [Fortune Ikechi](https://blog.logrocket.com/author/fortuneikechi/)✏️**

Music players are devices or applications that allow you to listen to audio files and recordings. There are many music players available, but in this article, we’ll build a clone of the [popular music streaming service, Spotify](https://www.spotify.com/us/), using React and [ts-audio](https://www.npmjs.com/package/ts-audio). 

You might expect that this tutorial would use [the Spotify API](https://developer.spotify.com/documentation/web-api/), however, Spotify and other music databases do not provide a streamable link or URL in their response body. The Spotify API does provide a preview URL, but the duration of the songs is limited to only 30 seconds, and that isn't enough for our example. Therefore, we won't be using the Spotify API or making any requests to any music API or databases. 

Instead, we’ll work with dummy data consisting of songs and image art. However, if you venture across an API with a streamable link, you can also apply the methods used in this article. You can find the complete code [for this tutorial at the GitHub repo](https://github.com/iamfortune/Spotify-clone-with-Ts-audio). Let’s get started!

*   [What is ts-audio?](#what-is-ts-audio)
*   [Building a Spotify clone with ts-audio](#building-a-spotify-clone-with-tsaudio)
    *   [Using the `Audio` component](#using-the-audio-component)
    *   [Using the `AudioPlaylist` component](#using-the-audio-playlist-component)
    *   [Creating the functionalities](#creating-functionalities)
*   [Problem solving: Mismatched song details](#problem-solving-mismatched-song-details)
*   [Adding styling](#adding-styling)

## What is ts-audio? <a name="what-is-ts-audio"></a>

ts-audio is an agnostic library that makes the [`AudioContext` API](https://developer.mozilla.org/en-US/docs/Web/API/AudioContext) easier to interact with. ts-audio provides you with methods like play, pause, and more, and it allows you to create playlists. ts-audio offers the following features:

*   Includes a simple API that abstracts the complexity of the `AudioContext` API
*   Offers cross-browser support
*   Makes it easy to create an audio playlist
*   Works with any language that compiles into JavaScript

## Building a Spotify clone with ts-audio <a name="building-a-spotify-clone-with-ts-audio"></a>

Let's start by creating a new React app with the command below:

```bash
npx create-react-app ts-audio
```

If you’re using Yarn, run the command below:

```bash
yarn create react-app ts-audio
```

For the rest of the tutorial, I’ll use Yarn. Next, we install the ts-audio package as follows:

```bash
yarn add ts-audio
```

At its core, ts-audio has two components, `Audio` and `AudioPlaylist`. The components are functions that we can call with specific parameters.

### Using the `Audio` component <a name="using-the-audio-component"></a>

The `Audio` component allows us to pass in a single song to be played. It also provides us with certain methods like `play()`, `pause()`, `stop()`, and more:

```javascript
// App.js

import Audio from 'ts-audio';
import Lazarus from './music/Lazarus.mp3';

export default function App() {
  const audio = Audio({
    file: Lazarus
  })

  const play = () => {
    audio.play()
  }

    const pause = () => {
    audio.pause()
  }

    const stop = () => {
    audio.stop()
  }

  return (
    <>
      <button onClick={play}>Play</button>
      <button onClick={pause}>Pause</button>
      <button onClick={stop}>Stop</button>
    </>
  )
}
```

In the code block above, we imported the `Audio` component from ts-audio and the song we want to play. We created an audio instance, set it to the imported `Audio` component, and then passed the imported music to the file parameter exposed by the `Audio` element. We took advantage of the methods provided to us by ts-audio, like `play()` and `pause()`, then passed them through functions to the buttons.

### Using the `AudioPlaylist` component <a name="using-the-audio-playlist-component"></a>

The `AudioPlaylist` component allows us to pass in multiple songs, but they have to be in an array, otherwise ts-audio won't play them. The `AudioPlaylist` component provides us with methods like `play()`, `pause()`, `stop()`, `next()`, and `prev()`. 

The code block below is an example of how to use the `AudioPlaylist` component:

```javascript
// App.js

import { AudioPlaylist } from 'ts-audio';
import Lazarus from './music/Lazarus.mp3';
import Sia from './music/Sia - Bird Set Free.mp3';

export default function App() {
  const playlist = AudioPlaylist({
    files: [Lazarus, Sia]
  })

  const play = () => {
    playlist.play()
  }

  const pause = () => {
    playlist.pause()
  }

  const next = () => {
    playlist.next()
  }

  const previous = () => {
    playlist.prev()
  }

  const stop = () => {
    playlist.stop()
  }

  return (
    <>
      <button onClick={play}>Play</button>
      <button onClick={pause}>Pause</button>
      <button onClick={next}>Next</button>
      <button onClick={prev}>Prev</button>
      <button onClick={stop}>Stop</button>
    </>
  )
}
```

The music player will have the following functionalities:

*   Change the artist to the current song's artist whenever we click on either **next** or **previous**
*   Change the image to the current song's image
*   Change the song title to the current song

In the `src` folder, create two folders called `images` and `music`, respectively. Navigate to the `images` folder and paste any photos you might need. In the `music` folder, you can paste any audio files that you want to use. 

In the following GitHub repos, you can get the [image files used in this tutorial](https://github.com/iamfortune/Spotify-clone-with-Ts-audio/tree/main/src/images) and [obtain the audio files](https://github.com/iamfortune/Spotify-clone-with-Ts-audio/tree/main/src/images). Next, import songs and images into `App.js` as follows:

```javascript
import { AudioPlaylist } from 'ts-audio';

// Music import
import Eyes from './music/01\. Jon Bellion - Eyes To The Sky.mp3';
import Mood from './music/24kGoldn-Mood-Official-Audio-ft.-Iann-Dior.mp3';
import Audio from './music/audio.mp3';
import Broken from './music/Cant Be Broken .mp3';
import Lazarus from './music/Lazarus.mp3';
import Sia from './music/Sia - Bird Set Free.mp3';
import Nobody from './music/T-Classic-Nobody-Fine-Pass-You.mp3';
import Yosemite from './music/Yosemite.mp3';

// Pictures import
import EyesImg from './images/Eyes to the sky.jpeg';
import MoodImg from './images/mood.jpeg';
import AudioImg from './images/lana.jpeg';
import BrokenImg from './images/lil wayne.jpeg';
import LazarusImg from './images/dave.jpeg';
import SiaImg from './images/sia.jpeg';
import NobodyImg from './images/nobody.jpeg';
import YosemiteImg from './images/travis.jpeg';

export default function App() {
  const songs =  [
      {
        title: 'Eyes to the sky',
        artist: 'Jon Bellion',
        img_src: EyesImg,
        src: Eyes,
      },
      {
        title: 'Lazarus',
        artist: 'Dave',
        img_src: LazarusImg,
        src: Lazarus,
      },
      {
        title: 'Yosemite',
        artist: 'Travis scott',
        img_src: YosemiteImg,
        src: Yosemite,
      },
      {
        title: 'Bird set free',
        artist: 'Sia',
        img_src: SiaImg,
        src: Sia,
      },
      {
        title: 'Cant be broken',
        artist: 'Lil wayne',
        img_src: BrokenImg,
        src: Broken,
      },
      {
        title: 'Mood',
        artist: '24kGoldn',
        img_src: MoodImg,
        src: Mood,
      },
      {
        title: 'Nobody fine pass you',
        artist: 'T-Classic',
        img_src: NobodyImg,
        src: Nobody,
      },
      {
        title: 'Dark paradise',
        artist: 'Lana Del Ray',
        img_src: AudioImg,
        src: Audio,
      },
    ]

  const playlist = AudioPlaylist({
      files: songs.map((song) => song.src),
    });

  const handlePlay = () => {
    playlist.play();
  };

  const handlePause = () => {
    playlist.pause();
  };

  const handleSkip = () => {
    playlist.next();
  };

  const handlePrevious = () => {
    playlist.prev();
  };

  return (
    <>
      <button onClick={handlePlay}>Play</button>
      <button onClick={handlePause}>Pause</button>
      <button onClick={handleSkip}>Next</button>
      <button onClick={handlePrevious}>Prev</button>     
    </>
  );
}
```

In the code block above, we imported the songs and images. Next, we created a song array containing objects. Each object has a `title`, `artist`, `img_src` for the imported images, and `src` for the imported songs. 

After that, we mapped through the song array to get to the song's `src`, which we passed into the files parameter. Remember, we have to pass it in as an array, but then the `map()` method creates a new array from calling a function. Therefore, we can pass it to the `files` parameter. 

We also created our methods and passed them to the various buttons. We’ll create a `Player.js` file to handle the buttons while we take care of the functionality in `App.js`:

```javascript
// Player.js

export default function Player({ play, pause, next, prev }) {
  return (
    <div className="c-player--controls">
      <button onClick={play}>Play</button>
      <button onClick={pause}>Pause</button>
      <button onClick={next}>Next</button>
      <button onClick={prev}>Previous</button> 
    </div>
  );
}
```

In the code block above, we created a `Player.js` file, then caught the props coming from `App.js`, and finally passed them into the buttons.

### Creating the functionalities <a name="creating-functionalities"></a>

To create the functionalities for our application, we import `useState` to get the current index of the song. We then set the image to the current photo, the artist to the current artist, and the title to the current title:

```javascript
// App.js

import React, { useState } from 'react';
import Player from './Player';
import { AudioPlaylist } from 'ts-audio';
// Music import

// Pictures import

export default function App() {
  const [currentSong, setCurrentSong] = useState(0);
  const [isPlaying, setIsPlaying] = useState(false);

  // Songs Array

  const playlist =AudioPlaylist({
      files: songs.map((song) => song.src),
    });

  const handlePlay = () => {
    playlist.play();
    setIsPlaying(true);
  };

  const handlePause = () => {
    playlist.pause();
    setIsPlaying(false);
  };

  const handleSkip = () => {
    playlist.next();
    setIsPlaying(true);
    setCurrentSong(
      (currentSong) => (currentSong + 1 + songs.length) % songs.length
    );
  };

  const handlePrevious = () => {
    playlist.prev();
    setIsPlaying(true);
    setCurrentSong(
      (currentSong) => (currentSong - 1 + songs.length) % songs.length
    );
  };
  return (
    <>
      <div className="App">
        <div className="c-player">
          <div className="c-player--details">
            {' '}
            <div className="details-img">
              {' '}
              <img src={songs[currentSong].img_src} alt="img" />
            </div>
            <h1 className="details-title">{songs[currentSong].title}</h1>
            <h2 className="details-artist">{songs[currentSong].artist}</h2>
          </div>
          <Player
            play={handlePlay}
            pause={handlePause}
            isPlaying={isPlaying}
            setIsPlaying={setIsPlaying}
            next={handleSkip}
            prev={handlePrevious}
          />
        </div>
      </div>
    </>
  );
}
```

We created a state event and set it to zero. When we click the **next** button, we set the state to the sum of the remainder of the current state, one, and the song's length, divided by the song's length:

```javascript
currentSong + 1 + songs.length) % songs.length
```

When we click the **previous** button, we set the state to the remainder of the current song, minus one, plus the song's length divided by the song's length:

```javascript
currentSong - 1 + songs.length) % songs.length
```

We also created a state event that checks if the song is playing or not, and then we passed it as props to the `Player` component. Finally, we handled the functionalities for changing the image, artists, and song title.

When we start the application, everything seems to work; the images change when clicking on the **next** button. However, the songs playing don't match the pictures and artist names displayed on the screen. Sometimes, two or more songs are playing simultaneously.

## Problem solving: Mismatched song details

When we click on the **next** or **previous** buttons, we are recalculating values and effectively causing a re-render. To stop this, we wrap the song array and the created instance of the playlist in a `useMemo` Hook, as seen below:

```javascript
// App.js

import React, { useState, useMemo } from 'react';
import Player from './Player';
import { AudioPlaylist } from 'ts-audio';
// Music import

// Pictures import

export default function App() {
  const [currentSong, setCurrentSong] = useState(0);

  const songs = useMemo(
    () => [
      {
        title: 'Eyes to the sky',
        artist: 'Jon Bellion',
        img_src: EyesImg,
        src: Eyes,
      },
      {
        title: 'Lazarus',
        artist: 'Dave',
        img_src: LazarusImg,
        src: Lazarus,
      },
      {
        title: 'Yosemite',
        artist: 'Travis scott',
        img_src: YosemiteImg,
        src: Yosemite,
      },
      {
        title: 'Bird set free',
        artist: 'Sia',
        img_src: SiaImg,
        src: Sia,
      },
      {
        title: 'Cant be broken',
        artist: 'Lil wayne',
        img_src: BrokenImg,
        src: Broken,
      },
      {
        title: 'Mood',
        artist: '24kGoldn',
        img_src: MoodImg,
        src: Mood,
      },
      {
        title: 'Nobody fine pass you',
        artist: 'T-Classic',
        img_src: NobodyImg,
        src: Nobody,
      },
      {
        title: 'Dark paradise',
        artist: 'Lana Del Ray',
        img_src: AudioImg,
        src: Audio,
      },
    ],
    []
  );

  const playlist = useMemo(() => {
    return AudioPlaylist({
      files: songs.map((song) => song.src),
    });
  }, [songs]);
```

The `useMemo` Hook effectively caches the value so that it doesn't need to be recalculated and therefore doesn't cause a re-render.

## Adding styling

We’ll use icons from [Font Awesome Icons](https://fontawesome.com/v5/docs/web/use-with/react) in this tutorial. You can install the Font Awesome package using the commands below:

```
yarn add @fortawesome/fontawesome-svg-core
yarn add @fortawesome/free-solid-svg-icons
yarn add @fortawesome/react-fontawesome
```
Copy and paste the code below into the `Player.js` file:

```javascript
// Player.js

import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPlay, faPause, faForward, faBackward } from '@fortawesome/free-solid-svg-icons';
export default function Player({ play, pause, next, prev, isPlaying, setIsPlaying }) {
  return (
    <div className="c-player--controls">
      <button className="skip-btn" onClick={prev}>
        <FontAwesomeIcon icon={faBackward} />
      </button>
      <button
        className="play-btn"
        onClick={() => setIsPlaying(!isPlaying ? play : pause)}
      >
        <FontAwesomeIcon icon={isPlaying ? faPause : faPlay} />
      </button>
      <button className="skip-btn" onClick={next}>
        <FontAwesomeIcon icon={faForward} />
      </button>
    </div>
  );
}
```

In the code block above, we get the props from the `App.js` file, then handle them inside the `Player.js` file. For styling, copy and paste the code below into your `index.css` file:

```javascript
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: 'Fira Sans', sans-serif;
}
body {
  background-color: #ddd;
}
.App {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  max-width: 100vw;
}
.c-player {
  display: block;
  background-color: #0a54aa;
  max-width: 400px;
  display: block;
  margin: 0px auto;
  padding: 50px;
  border-radius: 16px;
  box-shadow: inset -6px -6px 12px rgba(0, 0, 0, 0.8),
    inset 6px 6px 12px rgba(255, 255, 255, 0.4);
}
.c-player > h4 {
  color: #fff;
  font-size: 14px;
  text-transform: uppercase;
  font-weight: 500;
  text-align: center;
}
.c-player > p {
  color: #aaa;
  font-size: 14px;
  text-align: center;
  font-weight: 600;
}
.c-player > p span {
  font-weight: 400;
}
.c-player--details .details-img {
  position: relative;
  width: fit-content;
  margin: 0 auto;
}
.c-player--details .details-img img {
  display: block;
  margin: 50px auto;
  width: 100%;
  max-width: 250px;
  border-radius: 50%;
  box-shadow: 6px 6px 12px rgba(0, 0, 0, 0.8),
    -6px -6px 12px rgba(255, 255, 255, 0.4);
}
.c-player--details .details-img:after {
  content: '';
  display: block;
  position: absolute;
  top: -25px;
  left: -25px;
  right: -25px;
  bottom: -25px;
  border-radius: 50%;
  border: 3px dashed rgb(255, 0, 0);
}
.c-player--details .details-title {
  color: #eee;
  font-size: 28px;
  text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8),
    -2px -2px 4px rgba(255, 255, 255, 0.4);
  text-align: center;
  margin-bottom: 10px;
}
.c-player--details .details-artist {
  color: #aaa;
  font-size: 20px;
  text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8),
    -2px -2px 4px rgba(255, 255, 255, 0.4);
  text-align: center;
  margin-bottom: 20px;
}
.c-player--controls {
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 30px;
}
.c-player--controls .play-btn {
  display: flex;
  margin: 0 30px;
  padding: 20px;
  border-radius: 50%;
  box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.8),
    -4px -4px 10px rgba(255, 255, 255, 0.4),
    inset -4px -4px 10px rgba(0, 0, 0, 0.4),
    inset 4px 4px 10px rgba(255, 255, 255, 0.4);
  border: none;
  outline: none;
  background-color: #ff0000;
  color: #fff;
  font-size: 24px;
  cursor: pointer;
}
.c-player--controls .skip-btn {
  background: none;
  border: none;
  outline: none;
  cursor: pointer;
  color: rgb(77, 148, 59);
  font-size: 18px;
}
```

## Conclusion

In this article, we’ve learned about ts-audio, an agnostic, easy-to-use library that works with the`AudioContext` API. We learned about ts-audio's methods and how it makes it easier to work with audio files. Finally, we learned how to build a working music player using ts-audio.

---

## Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, [try LogRocket](https://www2.logrocket.com/react-performance-monitoring). 

[![LogRocket signup](https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)](https://www2.logrocket.com/react-performance-monitoring)

[LogRocket](https://www2.logrocket.com/react-performance-monitoring) is like a DVR for web and mobile apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — [start monitoring for free](https://www2.logrocket.com/react-performance-monitoring).