Creating a Responsive Spotify app using React

Author

Full Stack Developer

Creating a Responsive Spotify app using React

November 18, 2021

reactexpress

Creating a Responsive Spotify app using React

We all love Spotify for what it is, but what if we could make a better version? In this post, We will show you how to create a Spotify clone that not only has many of Spotify’s features but also includes a lyric lookup feature for any song you want.

By the end of this project, you will have implemented the following features

1. A login with Spotify button.
2. Searchfield to find songs and artists.
3. A Spotify music player(Spotify Premium required).
4. Displaying lyrics for songs looked up.

Prerequisites

  • HTML
  • CSS
  • Javascript + ES6
  • React
  • Styled components(Optional)
  • Express and Node.js(basics)

Project Setup

For this app, we will be having two folders, a client and a server folder. The client folder will contain all the react files, while the server folder will have a simple express app that will work as a server for the react app to make requests.

In the client folder, initialize the react project by running the following command

npx creat-react-app spotify-app

Once you have created your react app, install the following packages

npm i axios react-spotify-web-playback spotify-web-api-node styled-components

In the server folder, initialize the express server by running the following command

npm init --y

Once the package.json file is created, you can install the following packages

npm i express cors dotenv lyrics-finder spotify-web-api-node

Getting started

Registering your application in Spotify for developers

Before we start coding, let us first register our application with Spotify. Firstly got to the Spotify for Developers Site and navigate to the Dashboard .

Over there you will be asked to log in, you can log in using your existing Spotify account if you have one or create a new one.

Once you are logged in, press the Create an App Button, add an app name, description, agree to the terms and conditions and press Create .

create-an-app

You will now be able to see your app on the Dashboard .

dashboard

Click on your respective app to get the Client ID and the Client Secret which we will be using in our app to make requests to the Spotify API.

overview

Lastly, on the overview page, click on the Edit Settings Button and add the Redirect URI which in our case will be http://localhost:3000 and click Save .

redirect-uri

That’s it for setting up our app on Spotify.

Creating the Express Server for the React application

The server we will be using will be relatively simple so let us get this out of the way before we start working on the react application.

We will require only one file which will be called index.js , but before that let us make a small change in the package.json file. Under "main": "index.js" add the following line "type": "module" , this will allow us to export packages in the index.js file using the ES6 format.

  "main": "index.js",
  "type": "module",

Once this is done, we can create the index.js file and add the following code below

import express from "express"
import cors from "cors"
import lyricsFinder from "lyrics-finder"
import SpotifyWebApi from "spotify-web-api-node"
import dotenv from "dotenv"

const app = express()
dotenv.config()

app.use(cors())
app.use(express.json())
app.use(express.urlencoded({ extended: true }))

const PORT = 3001

app.post("/login", async (req, res) => {
  const { code } = req.body
  const spotifyApi = new SpotifyWebApi({
    redirectUri: process.env.REDIRECT_URI,
    clientId: process.env.CLIENT_ID,
    clientSecret: process.env.CLIENT_SECRET,
  })

  try {
    const {
      body: { access_token, refresh_token, expires_in },
    } = await spotifyApi.authorizationCodeGrant(code)

    res.json({ access_token, refresh_token, expires_in })
  } catch (err) {
    console.log(err)
    res.sendStatus(400)
  }
})

app.post("/refresh", async (req, res) => {
  const { refreshToken } = req.body
  const spotifyApi = new SpotifyWebApi({
    redirectUri: process.env.REDIRECT_URI,
    clientId: process.env.CLIENT_ID,
    clientSecret: process.env.CLIENT_SECRET,
    refreshToken,
  })

  try {
    const {
      body: { access_token, expires_in },
    } = await spotifyApi.refreshAccessToken()
    res.json({ access_token, expires_in })
  } catch (err) {
    console.log(err)
    res.sendStatus(400)
  }
})

app.get("/lyrics", async (req, res) => {
  const { artist, track } = req.query
  const lyrics = (await lyricsFinder(artist, track)) || "No Lyrics Found"
  res.json({ lyrics })
})

app.listen(PORT, err => {
  if (err) console.log(err)
  console.log("listening on port", PORT)
})

Let us now walk through what is going on here.

The initial code is just the setup to get express working and configuring the PORT for the server to listen to, which in our case is 3001.

The 3 routes that we are using here are login , refresh and lyrics , they are doing the following

login - This route will help us ensure the correct user is logged in from the react app and help in making any request. Here we will get the code from the front end, this will be a unique ID for every user. We then have a variable called spotifyApi which is initialized from the class SpotifyWebApi that we get from the spotify-web-api-node package, we pass in the redirectUri , clientId and clientSecret values that we got from the Spotify Developers site. We can call the spotifyApi.authorizationCodeGrant(code) function with code as the argument from which we will get the access_token , refresh_token and expires_in values that we can use in the frontend.

refresh - If you noticed in the login route, we have an expires_in value being sent to the front end. This value will persist for a limited time and once it expires, the user is logged out. So the refresh route helps us maintain the user session and fetches a new expires_in value. This works similar to the login route except we get a refreshToken from the front end and in the spotifyApi class we also pass the refreshToken along with the previous values. We then call the spotifyApi.refreshAccessToken() function from which we will get the access_token and expires_in values that we can use in the frontend.

lyrics - This route makes use of the lyrics-finder package which will allow us to fetch the lyrics for the song we select in the front end. We get the artist and the track name from the front end which then is passed to the function lyricsFinder that we get from the lyrics-finder package. The value we get from this can then be returned to the front end.

Note: You may have noticed that we are using environment variables for values we get from the Spotify Developers site, this is a safer way to store sensitive data. This is why we are using the dotenv package.

You can do the same by creating a .env file like the following

REDIRECT_URI=http://localhost:3000
CLIENT_ID="Add your Client ID here"
CLIENT_SECRET="Add your Client Secret Here"

That’s it for the server, you can now start your server by running node index.js or nodemon index.js if you have the nodemon package installed as a global dependency.

Creating the React application

Now we can finally move on to the React application.

With regards to styling we will be using a package called styled-components which allows us to use component-level styles in a React application(Read more about it here). You could follow along with using styled-components too or use any other method of styling you are comfortable with.

You can check out this blog, You Can Style Better if you would like to get helpful tips on styling.

You can delete the files not required and follow the structure below

react-file-structure

Let us now go through all the components used

Index file

The index.jsx file

import React from "react"
import ReactDOM from "react-dom"
import App from "./App"
import GlobalStyles from "./styles/globalStyles.styles"

ReactDOM.render(
  <>
    <GlobalStyles />
    <App />
  </>,
  document.getElementById("root")
)

The GlobalStyles.styles.jsx file

import { createGlobalStyle } from "styled-components"

const GlobalStyles = createGlobalStyle`
html,
body {
  padding: 0;
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
    Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}

a {
  color: inherit;
  text-decoration: none;
}

* {
  box-sizing: border-box;
}
`

export default GlobalStyles

Here it is pretty much the same as it is by default except a <GlobalStyles /> component has been added which adds global CSS styles via styled-components

App Component

The App.jsx file

import React from "react"

import Dashboard from "./Dashboard"
import Login from "./Login"

import { Container } from "./styles/App.styles"

const App = () => {
  const code = new URLSearchParams(window.location.search).get("code")

  return <Container>{code ? <Dashboard code={code} /> : <Login />}</Container>
}

export default App

The App.styles.jsx file

import styled from "styled-components"

export const Container = styled.div`
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-color: #191414;
`

What we are doing here is conditionally rendering either the Login or Dashboard component based on the code value which we will fetch from the URL parameters, we will understand the code value better in the Login component.

Login Component

The Login.jsx file

import React from "react"

import { LoginButton, LoginLink } from "./styles/Login.styles"

const Login = () => {
  const AUTH_URL = `https://accounts.spotify.com/authorize?client_id=${process.env.REACT_APP_CLIENT_ID}&response_type=code&redirect_uri=${process.env.REACT_APP_REDIRECT_URI}&scope=streaming%20user-read-email%20user-read-private%20user-library-read%20user-library-modify%20user-read-playback-state%20user-modify-playback-state`

  return (
    <LoginButton>
      <LoginLink href={AUTH_URL}>Login with Spotify</LoginLink>
    </LoginButton>
  )
}

export default Login

The Login.styles.jsx file

import styled from "styled-components"

export const LoginButton = styled.button`
  background-color: #1db954;
  padding: 1rem;
  border: none;
  border-radius: 8px;
`

export const LoginLink = styled.a`
  color: #ffff;
  font-size: 1.5rem;
  font-weight: bold;
  text-decoration: none;
`

In the Login component, we have a href tag wrapped by a button, on click of the href tag we are redirected to the Log In to the Spotify page. From this page, we get the code params which will be used to get the access_token . The href tag that we use has the following

https://accounts.spotify.com/authorize?client_id=${process.env.REACT_APP_CLIENT_ID}&response_type=code&redirect_uri=${process.env.REACT_APP_REDIRECT_URI}&scope=streaming%20user-read-email%20user-read-private%20user-library-read%20user-library-modify%20user-read-playback-state%20user-modify-playback-state

https://accounts.spotify.com/authorize is the URL where we need to authenticate using Spotify. All Spotify Authentication requests must be passed through this URL.

The client_id is the Client ID provided to you by the Spotify Web API and you need to mention it here.

The response_type will be set to code , this means that on success of login, we will be returned a code value which is an authorization code that can be exchanged for an access_token .

The redirect_uri is the one which we gave in the Spotify Web API settings, this states where to take back the user if the Spotify login was successful.

The scopes are permissions that you need to ask Spotify for. More such permissions are available on Spotify API Documentation.

Before we talk about the Dashboard component, let us go through the smaller components that are used for it.

Player Component

The Player.jsx file

import React, { useState, useEffect } from "react"

import SpotifyPlayer from "react-spotify-web-playback"

const Player = ({ accessToken, trackUri }) => {
  const [play, setPlay] = useState(false)

  useEffect(() => {
    setPlay(true)
  }, [trackUri])

  if (!accessToken) return null
  return (
    <SpotifyPlayer
      token={accessToken}
      showSaveIcon
      callback={state => !state.isPlaying && setPlay(false)}
      play={play}
      uris={trackUri ? trackUri : []}
      styles={{
        activeColor: "#fff",
        bgColor: "#333",
        color: "#fff",
        loaderColor: "#fff",
        sliderColor: "#1cb954",
        trackArtistColor: "#ccc",
        trackNameColor: "#fff",
        height: "55px",
      }}
    />
  )
}

export default Player

We have the Player component which will allow us to play the music we select. We make use of the SpotifyPlayer component from the react-spotify-web-playback package which renders the music player.

This component takes accessToken and trackUri as prop values.

We make use of a useEffect hook to set our state value play to true if trackUri is present, this allows the music player to automatically play the song we select.

TrackSearchResults Component

The TrackSearchResults.jsx file

import React from "react"

import {
  ResultContainer,
  ResultImage,
  SongContainer,
  TitleText,
  ArtistText,
} from "./styles/TrackSearchResults.styles"

const TrackSearchResult = ({ track, chooseTrack }) => {
  function handlePlay() {
    chooseTrack(track)
  }

  return (
    <ResultContainer onClick={handlePlay}>
      <ResultImage src={track.albumUrl} />
      <SongContainer>
        <TitleText>{track.title}</TitleText>
        <ArtistText>{track.artist}</ArtistText>
      </SongContainer>
    </ResultContainer>
  )
}

export default TrackSearchResult

The TrackSearchResults.styles.jsx file

import styled from "styled-components"

export const ResultContainer = styled.div`
  display: flex;
  align-items: center;
  margin: 2rem;
  cursor: pointer;
`

export const ResultImage = styled.img`
  height: 64px;
  width: 64px;
`

export const SongContainer = styled.div`
  margin-left: 3rem;
`

export const TitleText = styled.p`
  color: #fff;
`

export const ArtistText = styled.p`
  color: #f5f5f5;
`

The TrackSearchResult component will render all songs/playlists searched from the Dashboard component. This is will take two props which are track and chooseTrack , track will provide all the details such as track album image, track title and artist name which we can display and chooseTrack is a callback function that helps the Dashboard component know what song to play.

UseAuth Hook

The useAuth.jsx file

import { useState, useEffect } from "react"
import axios from "axios"

const useAuth = code => {
  const [accessToken, setAccessToken] = useState()
  const [refreshToken, setRefreshToken] = useState()
  const [expiresIn, setExpiresIn] = useState()

  useEffect(() => {
    ;(async () => {
      try {
        const {
          data: { access_token, refresh_token, expires_in },
        } = await axios.post(`${process.env.REACT_APP_BASE_URL}/login`, {
          code,
        })
        setAccessToken(access_token)
        setRefreshToken(refresh_token)
        setExpiresIn(expires_in)
        window.history.pushState({}, null, "/")
      } catch {
        window.location = "/"
      }
    })()
  }, [code])

  useEffect(() => {
    if (!refreshToken || !expiresIn) return
    const interval = setInterval(async () => {
      try {
        const {
          data: { access_token, expires_in },
        } = await axios.post(`${process.env.REACT_APP_BASE_URL}/refresh`, {
          refreshToken,
        })
        setAccessToken(access_token)
        setExpiresIn(expires_in)
      } catch {
        window.location = "/"
      }
    }, (expiresIn - 60) * 1000)

    return () => clearInterval(interval)
  }, [refreshToken, expiresIn])

  return accessToken
}

export default useAuth

Lastly, before we get started with the Dashboard component, let us go through the custom hook called useAuth which will handle the authentication.

In this hook, we are maintaining 3 states, which are the accessToken , refreshToken , expiresIn values

We have 2 useEffect hooks being used, they are explained below.

The first one is used to make an API call to the login route, we will send the code value as the request body and on success of this route, we will receive the access_token , refresh_token and expires_in values which we can assign to our respective state values. We can then clear the parameter values which would contain the code parameter as it would be unsafe to let this persist.

The second one is used to make an API call to the refresh route, This call is required because once the expiresIn value expires, our accessToken is invalid so we can use our refreshToken to fetch a new accessToken and expiresIn value. We pass the refreshToken as the body here and on success of this route, we will receive the access_token and expires_in values which we can assign to our respective state values.

This hook will return the accessToken which will be used by the Spotify web player and for fetching artists and songs.

Dashboard Component

The Dashboard.jsx file

import React, { useState, useEffect } from "react"

import useAuth from "./hooks/useAuth"
import Player from "./Player"
import TrackSearchResult from "./TrackSearchResult"
import SpotifyWebApi from "spotify-web-api-node"
import axios from "axios"
import {
  DashBoardContainer,
  SearchInput,
  ResultsContainer,
  LyricsContainer,
  PlayerContainer,
} from "./styles/Dashboard.styles"

const spotifyApi = new SpotifyWebApi({
  clientId: process.env.REACT_APP_CLIENT_ID,
})

const Dashboard = ({ code }) => {
  const accessToken = useAuth(code)
  const [search, setSearch] = useState("")
  const [searchResults, setSearchResults] = useState([])
  const [playingTrack, setPlayingTrack] = useState()
  const [lyrics, setLyrics] = useState("")

  function chooseTrack(track) {
    setPlayingTrack(track)
    setSearch("")
    setLyrics("")
  }

  useEffect(() => {
    if (!playingTrack) return
    ;(async () => {
      const {
        data: { lyrics },
      } = await axios.get(`${process.env.REACT_APP_BASE_URL}/lyrics`, {
        params: {
          track: playingTrack.title,
          artist: playingTrack.artist,
        },
      })
      setLyrics(lyrics)
    })()
  }, [playingTrack])

  useEffect(() => {
    if (!accessToken) return
    spotifyApi.setAccessToken(accessToken)
  }, [accessToken])

  useEffect(() => {
    if (!search) return setSearchResults([])
    if (!accessToken) return

    let cancel = false
    ;(async () => {
      const { body } = await spotifyApi.searchTracks(search)
      if (cancel) return
      setSearchResults(
        body.tracks.items.map(track => {
          const smallestAlbumImage = track.album.images.reduce(
            (smallest, image) => {
              if (image.height < smallest.height) return image
              return smallest
            },
            track.album.images[0]
          )

          return {
            artist: track.artists[0].name,
            title: track.name,
            uri: track.uri,
            albumUrl: smallestAlbumImage.url,
          }
        })
      )
    })()

    return () => (cancel = true)
  }, [search, accessToken])

  return (
    <DashBoardContainer>
      <SearchInput
        type="search"
        placeholder="Search Songs/Artists"
        value={search}
        onChange={e => setSearch(e.target.value)}
      />
      <ResultsContainer>
        {searchResults.map(track => (
          <TrackSearchResult
            track={track}
            key={track.uri}
            chooseTrack={chooseTrack}
          />
        ))}
        {searchResults.length === 0 && (
          <LyricsContainer>{lyrics}</LyricsContainer>
        )}
      </ResultsContainer>
      <PlayerContainer>
        <Player accessToken={accessToken} trackUri={playingTrack?.uri} />
      </PlayerContainer>
    </DashBoardContainer>
  )
}

export default Dashboard

The Dashboard.styles.jsx file

import styled from "styled-components"

export const DashBoardContainer = styled.div`
  padding: 2rem;
  min-height: 100vh;
  width: 100%;
`

export const SearchInput = styled.input`
  width: 100%;
  height: 55px;
  padding: 10px;
  border-radius: 5px;
  font-size: 1.5rem;

  &::placeholder {
    font-size: 1.5rem;
  }
`

export const ResultsContainer = styled.div`
  flex-grow: 1;
  margin: 3rem 0;
  overflow-y: auto;
  overflow-x: auto;
`

export const LyricsContainer = styled.div`
  height: 65vh;
  text-align: center;
  color: #fff;
  white-space: pre;
`

export const PlayerContainer = styled.div`
  width: 100%;
  position: fixed;
  bottom: 0;
  left: 0;
`

Now we can move on to the final component.

The Dashboard component maintains the following state values

The accessToken value will be returned from the useAuth hook where we pass the code as the argument. This value will be used by the Spotify web player and when fetching the artists/tracks.

The search state will maintain the value we type in when searching for a song/artist.

The searchResults state will contain an array of values of the artists/songs that we fetch on search.

The playingTrack state will contain the value of the song that we selected, so we can accordingly play it and fetch its lyrics.

The lyrics state will contain the lyrics of the song we selected, so it can be displayed.

There is a variable called spotifyApi which is initialized from the SpotifyWebApi class where we pass in our clientId. This class is from the spotify-web-api-node and this is required so we can use its respective functions to fetch artists/songs.

We have 3 useEffect hooks which are being used which are as follows

The first hook is triggered based on any changes in the playingTrack state, if this state has a value, the API call for fetching lyrics will be made using this state as its body. The value returned from this API call will be set to the lyrics state.

The second hook is triggered if there are any changes in the acccessToken state, This is to ensure that we have the correct accessToken value. if we do, we can set the access token for spotifyApi variable by doing this spotifyApi.setAccessToken(accessToken) .

The third hook is triggered if there are any changes in the accessToken or search state. If both of them are truthy values we can trigger the async function called spotifyApi.searchTracks(search) which is from the spotify-web-api-node package, this function will use the search state as an argument and fetch all the respective artists and songs based on this value. The response from this function will be an array that can be iterated on and its values can be set to the searchResultsState .

The components that are rendered are as follows

An input component is used for searching for artists/songs. This will help us set the search value.

The TrackSearchResults component will display the values from the searchResults state. It also triggers a function to set the playingTrack state based on the song clicked.

A container to display the lyrics when a song is selected.

The Player component will display the Spotify web music player. It takes the accessToken and the uri value of the playingTrack state as props.

Note: Similar to our server, we can also use environment variables in our react app. The only difference is that we should ensure that we add REACT_APP_ before our env properties

You can do the same by creating a .env file in the root of our client folder.

REACT_APP_CLIENT_ID="Add your Client ID here"
REACT_APP_REDIRECT_URI=http://localhost:3000
REACT_APP_BASE_URL=http://localhost:3001

That’s it for all the components in our React app, We have finished all that is required for this app!

We can now start our react project by running npm start in the terminal

Note: You may notice if we reload our app, we are redirected to log in again, This is because we are not persisting our access token. We could do so by setting it in the local storage but that is not a good practice as it is unsafe to store tokens here. We could instead use an oAuth mechanism , store it in our cookies , use a context management system or something along these lines. However, the main purpose of this post is to display a Spotify clone with a responsive design so we will leave it as it is for now.

Here is a small preview of the application

spotify-app-demo

You can find the complete source code here

References

Antstack Blog Post
Antstack Blog Post

Are you planning to go to serverless? AntStack is a cloud computing service and consulting company primarily focusing on Serverless Computing. We help companies get up and running with serverless, and we’ll make sure that there are no limits.

Keep track of our socials and connect with us - LinkedIn