Recreating Spotify using GraphQL & React Native

Recreating Spotify using GraphQL & React Native
In this post we look at how to recreate Spotify Playlists using the popular data query and manipulation language for APIs GraphQL and React Native

In 2015 Facebook released their internally built data query and manipulation language for APIs GraphQL and it has since risen to the forefront of our minds, in part due to the adoption by big names such as Twitter, Paypal and the New York Times. In fact, the State of JavaScript 2018 listed GraphQL second only to Redux as the world’s favourite Data Layer technology.

With that in mind, it’s useful to understand where GraphQL strengths and weaknesses lie. In this tutorial i’m going to guide you through creating a screen that hopefully a lot of you are familar with; the Spotify Playlist. With any luck you’ll get a better understanding of how GraphQL can benefit your applications and drive you to explore the technology in more detail.

Creating the View

As with any (good) application we’re going to need to piece together a great looking frontend experience to display our data in a way that appeals to our users, Spotify is no different. To get us going we’re going to use React Native to build our mobile application, running Expo to make development on our devices easier. We’ll get to the GraphQL part later on in this tutorial, once we have our application up and running.

First off we’ll install Expo and get our React Native template created. To install Expo simply run:

npm install -g expo-cli

Now to set up the folder structure and all the necessary files needed you run:

expo init spotify-playlist

  • When asked to ‘Choose a template’ select ‘blank
  • When prompted to “enter a few initial configuration values” change the application name to ‘Spotify Playlist

After the inital setup has completed you can start up your application by navigating to your newly created folder and running:

npm start

If you’re new to Expo it’s easy to get started. After running npm start you’ll be presented with all the instructions on how to test your application with your device/simulator. To test on your device it’s as simple as scanning the QR code using the Expo iOS/Android App.

Once running you should see the default screen saying “Open up App.js to start working on your app!”

Let’s start by creating the basic container for the application. The Spotify Playlist screen consists of two main elements, a header with a gradient background and a list of all the playlist items. To recreate these two elements open up your App.js and replace it with the following:

import React from 'react';
import { StyleSheet, ScrollView, FlatList } from 'react-native';
import { LinearGradient } from 'expo';

export default class App extends React.Component {
  render() {
    return (
      <ScrollView contentContainerStyle={styles.container}>
        <LinearGradient colors={['#3f6b6b', '#121212']} style={styles.header} />
        <FlatList style={styles.list} />
      </ScrollView>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    alignItems: 'center',
    justifyContent: 'center',
  },
  header: {
    width: '100%',
    height: 500
  },
  list: {
    width: '100%',
    height: 800,
    backgroundColor: '#121212'
  }
});

App.js

Here we’re creating the two elements; a <LinearGradient /> component from Expo and a <FlatList /> from React Native itself. We’ve added some basic styling and hardcoded the height values just for the time being (we’ll change this height to auto once we’ve got some data in).

Save your file, Expo should hot reload your application which should now look something like this:

We’re now ready to start inserting some placeholder data into our application to start getting the header elements styling correct. For this we’ll need two fonts gibson-regular and gibson-bold which you can download here and here. Add the fonts to the /assets folder in your project and we’re ready to start putting the header components together.

Because we are using external fonts we’ll need to load them asyncronously before rendering our components otherwise the fonts won’t yet exist and the application will error. Luckily Expo gives us a nice way of doing this. Add the following to App.js:

import React from 'react';
import { StyleSheet, ScrollView, FlatList, View, Image, Text, TouchableOpacity } from 'react-native';
import { Font, LinearGradient } from 'expo';

export default class App extends React.Component {
  state = { fontLoaded: false };

  async componentDidMount() {
    await Font.loadAsync({
      'gibson-regular': require('./assets/gibson-regular.ttf'),
      'gibson-bold': require('./assets/gibson-bold.ttf')
    });

    this.setState({ fontLoaded: true });
  }

  render() {
    return (
      <View>
        <ScrollView contentContainerStyle={styles.container}>
          <LinearGradient colors={['#3f6b6b', '#121212']} style={styles.header} />
          <FlatList style={styles.list} />
        </ScrollView>
        {this.state.fontLoaded ? (
          <View style={styles.playlistDetails}>
            <Image style={styles.playlistArt} source={{ uri: 'https://github.com/jamiemaison/hosted/blob/master/placeholder.jpg?raw=1' }} />

            <Text style={styles.playlistTitle}>Playlist Name</Text>
            <Text style={styles.playlistSubtitle}>{'BY USER • 000,000 FOLLOWERS'}</Text>
            <TouchableOpacity style={styles.playlistButton}><Text style={styles.playlistButtonText}>SHUFFLE PLAY</Text></TouchableOpacity>
          </View>)
          : null}
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    alignItems: 'center',
    justifyContent: 'center',
  },
  header: {
    width: '100%',
    height: 600
  },
  list: {
    width: '100%',
    height: 800,
    backgroundColor: '#121212'
  },
  playlistDetails: {
    width: '100%',
    height: 600,
    position: 'absolute',
    top: 90,
    display: 'flex',
    alignItems: 'center'
  },
  playlistArt: {
    width: 180,
    height: 180,
  },
  playlistTitle: {
    fontFamily: 'gibson-bold',
    color: '#fff',
    fontSize: 30,
    marginTop: 50
  },
  playlistSubtitle: {
    fontFamily: 'gibson-regular',
    color: '#b9bdbe',
    fontSize: 12,
    marginTop: 15,
    textTransform: 'uppercase'
  },
  playlistButton: {
    backgroundColor: '#2ab759',
    width: 230,
    height: 50,
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    borderRadius: 100,
    marginTop: 40
  },
  playlistButtonText: {
    fontFamily: 'gibson-bold',
    fontSize: 12,
    color: '#fff',
    letterSpacing: 2
  }
});

App.js

See how we load the font async in componentDidMount and then not render the relevant components until this.state.fontLoaded = true? You may also notice that our playlist items are not children of the <LinearGradient /> component we created earlier. This is because if you look at the Spotify app the playlist items move independently to the top gradient. This parallax effect is one of those subtle things that make some apps look so appealing.

Save your App.js, you should now have something that looks like this.

Recreating the Playlist

Now for the all important part of any playlist, the songs themselves! We’ll achieve this by using a React Native <FlatList />. Again, we’ll just add some mock data for the time being until we set up GraphQL. Modify your App.js to the following:

import React from 'react';
import { StyleSheet, ScrollView, FlatList, View, Image, Text, TouchableOpacity } from 'react-native';
import { Font, LinearGradient } from 'expo';

export default class App extends React.Component {
  state = { fontLoaded: false };

  async componentDidMount() {
    await Font.loadAsync({
      'gibson-regular': require('./assets/gibson-regular.ttf'),
      'gibson-bold': require('./assets/gibson-bold.ttf')
    });

    this.setState({ fontLoaded: true });
  }

  render() {
    return (
      <View>
        <ScrollView contentContainerStyle={styles.container}>
          <LinearGradient colors={['#3f6b6b', '#121212']} style={styles.header} />
          {this.state.fontLoaded ? (
          <FlatList style={styles.list}
            data={[{key: '0', title: 'Title', artist: 'Artist', album: 'Album'}, {key: '1', title: 'Title', artist: 'Artist', album: 'Album'}, {key: '2', title: 'Title', artist: 'Artist', album: 'Album'}, {key: '3', title: 'Title', artist: 'Artist', album: 'Album'}, {key: '4', title: 'Title', artist: 'Artist', album: 'Album'}, {key: '5', title: 'Title', artist: 'Artist', album: 'Album'}, {key: '6', title: 'Title', artist: 'Artist', album: 'Album'}, {key: '7', title: 'Title', artist: 'Artist', album: 'Album'}, {key: '8', title: 'Title', artist: 'Artist', album: 'Album'}, {key: '9', title: 'Title', artist: 'Artist', album: 'Album'}]}
            renderItem={({item}) => (
              <View style={styles.playlistItem}>
                <Text style={styles.playlistItemTitle}>{item.title}</Text>
                <Text style={styles.playlistItemMeta}>{`${item.artist} • ${item.album}`}</Text>
              </View>
            )}
          />) : null }
        </ScrollView>
        {this.state.fontLoaded ? (
          <View style={styles.playlistDetails}>
            <Image style={styles.playlistArt} source={{ uri: 'https://github.com/jamiemaison/hosted/blob/master/placeholder.jpg?raw=1' }} />

            <Text style={styles.playlistTitle}>Playlist Name</Text>
            <Text style={styles.playlistSubtitle}>{'BY USER • 000,000 FOLLOWERS'}</Text>
            <TouchableOpacity style={styles.playlistButton}><Text style={styles.playlistButtonText}>SHUFFLE PLAY</Text></TouchableOpacity>
          </View>)
          : null}
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    alignItems: 'center',
    justifyContent: 'center',
  },
  header: {
    width: '100%',
    height: 510
  },
  list: {
    width: '100%',
    height: 800,
    backgroundColor: '#121212'
  },
  playlistDetails: {
    width: '100%',
    height: 510,
    position: 'absolute',
    top: 90,
    display: 'flex',
    alignItems: 'center'
  },
  playlistArt: {
    width: 180,
    height: 180,
  },
  playlistTitle: {
    fontFamily: 'gibson-bold',
    color: '#fff',
    fontSize: 30,
    marginTop: 50
  },
  playlistSubtitle: {
    fontFamily: 'gibson-regular',
    color: '#b9bdbe',
    fontSize: 12,
    marginTop: 15
  },
  playlistButton: {
    backgroundColor: '#2ab759',
    width: 230,
    height: 50,
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    borderRadius: 100,
    marginTop: 40
  },
  playlistButtonText: {
    fontFamily: 'gibson-bold',
    fontSize: 12,
    color: '#fff',
    letterSpacing: 2
  },
  playlistItem: {
    marginLeft: 25,
    marginBottom: 25
  },
  playlistItemTitle: {
    fontFamily: 'gibson-bold',
    fontSize: 18,
    color: '#fff'
  },
  playlistItemMeta: {
    fontFamily: 'gibson-regular',
    color: '#b9bdbe',
    fontSize: 15
  }
});

App.js

In this code snippet we are iterating over our data and creating a simple view with a couple of Text components for each piece of data.

If you load up the real Spotify app now you’ll see that upon scrolling the view there’s a little trickery that goes on to hide the playlist text as your thumb moves up the screen as well as gently fading out the album art. There’s a few ways to achieve this effect but i’ll show you one way that uses a combination of CSS & JS which hopefully is simple.

Again, open up your App.js and we’ll make a few modifications so that it now looks like this:

import React from 'react';
import { StyleSheet, ScrollView, FlatList, View, Image, Text, TouchableOpacity } from 'react-native';
import { Font, LinearGradient } from 'expo';

export default class App extends React.Component {
  state = { fontLoaded: false, currentScrollPos: 0 };

  async componentDidMount() {
    await Font.loadAsync({
      'gibson-regular': require('./assets/gibson-regular.ttf'),
      'gibson-bold': require('./assets/gibson-bold.ttf')
    });

    this.setState({ fontLoaded: true });
  }

  render() {
    return (
      <View>
        <ScrollView contentContainerStyle={styles.container} onScroll={(event) => {
          console.log(event.nativeEvent.contentOffset.y);
          this.setState({ currentScrollPos: event.nativeEvent.contentOffset.y })
        }}>
          <LinearGradient colors={['#3f6b6b', '#121212']} style={styles.header} />
          {this.state.fontLoaded ? (
            <FlatList style={styles.list}
              data={[{ key: '0', title: 'Title', artist: 'Artist', album: 'Album' }, { key: '1', title: 'Title', artist: 'Artist', album: 'Album' }, { key: '2', title: 'Title', artist: 'Artist', album: 'Album' }, { key: '3', title: 'Title', artist: 'Artist', album: 'Album' }, { key: '4', title: 'Title', artist: 'Artist', album: 'Album' }, { key: '5', title: 'Title', artist: 'Artist', album: 'Album' }, { key: '6', title: 'Title', artist: 'Artist', album: 'Album' }, { key: '7', title: 'Title', artist: 'Artist', album: 'Album' }, { key: '8', title: 'Title', artist: 'Artist', album: 'Album' }, { key: '9', title: 'Title', artist: 'Artist', album: 'Album' }]}
              renderItem={({ item }) => (
                <View style={styles.playlistItem}>
                  <Text style={styles.playlistItemTitle}>{item.title}</Text>
                  <Text style={styles.playlistItemMeta}>{`${item.artist} • ${item.album}`}</Text>
                </View>
              )}
            />) : null}
        </ScrollView>
        {this.state.fontLoaded ? (
          <View style={{...styles.playlistDetails, height: this.calculatePlaylistHeight()}} pointerEvents="none">
            <Image style={this.calculateArtSize()} source={{ uri: 'https://github.com/jamiemaison/hosted/blob/master/placeholder.jpg?raw=1' }} />

            {this.state.currentScrollPos < 103 ? <Text style={styles.playlistTitle}>Playlist Name</Text> : null}
            {this.state.currentScrollPos < 53 ? <Text style={styles.playlistSubtitle}>{'BY USER • 000,000 FOLLOWERS'}</Text> : null}
          </View>) : null}
          {this.state.fontLoaded ? (<TouchableOpacity style={{ ...styles.playlistButton, top: this.calculateButtonPos() }}><Text style={styles.playlistButtonText}>SHUFFLE PLAY</Text></TouchableOpacity>) : null }
      </View>
    );
  }

  calculatePlaylistHeight = () => {
    if (this.state.currentScrollPos > 173) {
      const height = 160 - (this.state.currentScrollPos - 172);
      return height >= 0 ? height : 0;
    }
    return 510; 
  }

  calculateButtonPos = () => {
    return this.state.currentScrollPos < 376 ? 420 - this.state.currentScrollPos : 44;
  }

  calculateArtSize = () => {
    return {
      width: (185 - (this.state.currentScrollPos / 10)),
      height: (185 - (this.state.currentScrollPos / 10)),
      opacity: (1 - (this.state.currentScrollPos / 350))
    };
  }
}

const styles = StyleSheet.create({
  container: {
    alignItems: 'center',
    justifyContent: 'center',
  },
  header: {
    width: '100%',
    height: 510
  },
  list: {
    width: '100%',
    height: 800,
    backgroundColor: '#121212'
  },
  playlistDetails: {
    width: '100%',
    position: 'absolute',
    top: 90,
    display: 'flex',
    alignItems: 'center',
    overflow: 'hidden'
  },
  playlistTitle: {
    fontFamily: 'gibson-bold',
    color: '#fff',
    fontSize: 30,
    marginTop: 50
  },
  playlistSubtitle: {
    fontFamily: 'gibson-regular',
    color: '#b9bdbe',
    fontSize: 12,
    marginTop: 15
  },
  playlistButton: {
    backgroundColor: '#2ab759',
    width: 230,
    height: 50,
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    borderRadius: 100,
    position: 'absolute',
    left: 90
  },
  playlistButtonText: {
    fontFamily: 'gibson-bold',
    fontSize: 12,
    color: '#fff',
    letterSpacing: 2
  },
  playlistItem: {
    marginLeft: 25,
    marginBottom: 25
  },
  playlistItemTitle: {
    fontFamily: 'gibson-bold',
    fontSize: 18,
    color: '#fff'
  },
  playlistItemMeta: {
    fontFamily: 'gibson-regular',
    color: '#b9bdbe',
    fontSize: 15
  }
});

App.js

Great! We should now have a fully functioning app that behaves just like the playlist section in the Spotify app. A few things to note about the above code:

  • Upon scroll of the view we save the currentScrollPos in state to be read by each of our components.
  • We then use React to hide the various text components when currentScrollPos is more than a certain value - { this.state.currentScrollPos < 53 ? <Text> : null } .
  • To make things seem a little less like voodoo and more readable we seperate some of the calculations into seperate functions: calculatePlaylistHeight, calculateButtonPos & calculateArtSize.

Now that we’ve got the app looking and behaving how we want it’s now time to start introducing GraphQL to the equation to handle the data!

Data Insertion using GraphQL

Before we get into the how let’s understand the what and why. GraphQL is sold as “front end queries made easy”. At its very core, GraphQL is a typed query language that gives developers an easier way to describe and ultimately obtain the data they need.

For the purposes of this tutorial we need some sort of server running where the actual data is going to be stored. This is easily enough done using apollo-server however you’re not here to start writing server code! So just like magic, i’ve created a Apollo Server playground on codesandbox.io that we can use for the purposes of this example. If you’re interested in the code you can view it here however if you just want to skip ahead all you need to do is open your favourite browser and navigate to https://rqvj0qw34.sse.codesandbox.io/ - launching this will start the server at that address so be sure to leave it open in the background for the duration of your development!

Now, back to our Spotify App — we’ll want to install the following dependencies for the next part of the tutorial:

  • apollo-boost — It is a zero configuration way of getting started with GraphQL in react.
  • react-apollo — Apollo client is the best way to use GraphQL in client-side applications. React-apollo provides an integration between GraphQL and Apollo client
  • graphql-tag — A JavaScript template literal tag that parses GraphQL queries
  • graphql — The JavaScript reference implementation for GraphQL. Needed to resolve a conflict with Expo.

Which can be installed by running:

npm install --save apollo-boost react-apollo graphql-tag graphql

Now that is sorted let’s get our data from our GraphQL server and display it using the following data query:

{
    playlist {
        name
        creator
        followers
        albumArt
        songs {
            key
            title
            artist
            album
       }
    }
}

GraphQL Query

Hopefully you can see how simple GraphQL makes data queries look, in this example we’re simply requesting one playlist object which has the properties name, creator, followers, albumArt and songs and that songs have key, title, artist and album values.

Let’s fetch and display this data using react-apollo by modiying our App.js to the following:

import React from 'react';
import { StyleSheet, ScrollView, FlatList, View, Image, Text, TouchableOpacity } from 'react-native';
import { Font, LinearGradient } from 'expo';
import ApolloClient from 'apollo-boost';
import { ApolloProvider } from 'react-apollo';
import { Query } from 'react-apollo'
import gql from 'graphql-tag'

const client = new ApolloClient({
  uri: 'https://rqvj0qw34.sse.codesandbox.io/',
})

export default class App extends React.Component {
  state = { fontLoaded: false, currentScrollPos: 0 };

  async componentDidMount() {
    await Font.loadAsync({
      'gibson-regular': require('./assets/gibson-regular.ttf'),
      'gibson-bold': require('./assets/gibson-bold.ttf')
    });

    this.setState({ fontLoaded: true });
  }

  render() {
    return (
      <ApolloProvider client={client}>
        <Query
          query={gql`
              {
                playlist {
                  name
                  creator
                  followers
                  albumArt
                  songs {
                    key
                    title
                    artist
                    album
                  }
                }
              }
            `}
        >
          {({ loading, error, data }) => {
            if (loading || error) return <View />
            return <View>
              <ScrollView contentContainerStyle={styles.container} onScroll={(event) => {
                console.log(event.nativeEvent.contentOffset.y);
                this.setState({ currentScrollPos: event.nativeEvent.contentOffset.y })
              }}>
                <LinearGradient colors={['#3f6b6b', '#121212']} style={styles.header} />
                {this.state.fontLoaded ? (
                  <FlatList style={styles.list}
                    data={data.playlist.songs}
                    renderItem={({ item }) => (
                      <TouchableOpacity style={styles.playlistItem}>
                        <Text style={styles.playlistItemTitle}>{item.title}</Text>
                        <Text style={styles.playlistItemMeta}>{`${item.artist} • ${item.album}`}</Text>
                      </TouchableOpacity>
                    )}
                  />) : null}
              </ScrollView>
              {this.state.fontLoaded ? (
                <View style={{ ...styles.playlistDetails, height: this.calculatePlaylistHeight() }} pointerEvents="none">
                  <Image style={this.calculateArtSize()} source={{ uri: data.playlist.albumArt }} />

                  {this.state.currentScrollPos < 103 ? <Text style={styles.playlistTitle}>{data.playlist.name}</Text> : null}
                  {this.state.currentScrollPos < 53 ? <Text style={styles.playlistSubtitle}>{`BY ${data.playlist.creator} • ${data.playlist.followers} FOLLOWERS`}</Text> : null}
                </View>) : null}
              {this.state.fontLoaded ? (<TouchableOpacity style={{ ...styles.playlistButton, top: this.calculateButtonPos() }}><Text style={styles.playlistButtonText}>SHUFFLE PLAY</Text></TouchableOpacity>) : null}
            </View>
          }}
        </Query>
      </ApolloProvider>
    );
  }

  calculatePlaylistHeight = () => {
    if (this.state.currentScrollPos > 173) {
      const height = 160 - (this.state.currentScrollPos - 172);
      return height >= 0 ? height : 0;
    }
    return 510;
  }

  calculateButtonPos = () => {
    return this.state.currentScrollPos < 376 ? 420 - this.state.currentScrollPos : 44;
  }

  calculateArtSize = () => {
    return {
      width: (185 - (this.state.currentScrollPos / 10)),
      height: (185 - (this.state.currentScrollPos / 10)),
      opacity: (1 - (this.state.currentScrollPos / 350))
    };
  }
}

const styles = StyleSheet.create({
  container: {
    alignItems: 'center',
    justifyContent: 'center',
  },
  header: {
    width: '100%',
    height: 510
  },
  list: {
    width: '100%',
    backgroundColor: '#121212'
  },
  playlistDetails: {
    width: '100%',
    position: 'absolute',
    top: 90,
    display: 'flex',
    alignItems: 'center',
    overflow: 'hidden'
  },
  playlistTitle: {
    fontFamily: 'gibson-bold',
    color: '#fff',
    fontSize: 30,
    marginTop: 50
  },
  playlistSubtitle: {
    fontFamily: 'gibson-regular',
    color: '#b9bdbe',
    fontSize: 12,
    marginTop: 15
  },
  playlistButton: {
    backgroundColor: '#2ab759',
    width: 230,
    height: 50,
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    borderRadius: 100,
    position: 'absolute',
    left: 90
  },
  playlistButtonText: {
    fontFamily: 'gibson-bold',
    fontSize: 12,
    color: '#fff',
    letterSpacing: 2
  },
  playlistItem: {
    marginLeft: 25,
    marginBottom: 25,
    width: '90%'
  },
  playlistItemTitle: {
    fontFamily: 'gibson-bold',
    fontSize: 18,
    color: '#fff'
  },
  playlistItemMeta: {
    fontFamily: 'gibson-regular',
    color: '#b9bdbe',
    fontSize: 15
  }
});

App.js

In the above code we’re using the <Query /> component from react-apollo to request the data and on data being returned we’re rendering all of our received information. The component also allows us to handle loading or error state which we’re just rendering a blank view at the moment but you can see how this can be used to provide a greater user experience.

Save and run the application now, you should see a fully functioning Spotify Playlist application using data straight from your GraphQL server!

Further Reading

That’s about it for this tutorial, if you have any GraphQL or React quieres you can always reach out on Twitter or Email and i’ll do my best to help! For further reading here’s some other articles that I found interesting:

Got a project that you’re looking to get started, think you may need my help with something or simpy want to reach out? Get in touch!

30s ad

React JS and Redux Bootcamp - Master React Web Development

React Tutorial and Projects Course

PHP Symfony 4 API Platform + React.js Full Stack Masterclass

Beginner React (2019). Create a Movie Web App

Master JavaScript from Scratch (with jQuery and React JS)

Suggest:

Learn GraphQL with Laravel and Vue.js - Full Tutorial

Making It All Fit Together with React and GraphQL

React + TypeScript : Why and How

React Tutorial - Learn React - React Crash Course [2019]

Learn React - Full Course for Beginners - React Tutorial 2019

React Native vs Flutter