1

I'm learning React by making a Spotify clone and for now, what I'm trying to do is to show Spotify sections such as "last played songs", "top artists" and "top songs" through a component called Body.js.

I get the data from a Spotify official API library created by jmperez in a useEffect hook in the App.js component. Once I get the data from the API, I store it in an object called initialState in a file called reducer.js.

This reducer.js file contains the initial state and the reducer function for a custom hook called useDataLayer.js which is a useContext hook that passes as value a useReducer to all the branches of my program. This way what I do is update the initialState from App.js and access this object through the useDataLayer hook in the different branches of my program (among them the Body component).

The problem I am having now is that it is not rendering the three sections mentioned before in Spotify, but only shows me one which is the "top songs". The weird thing is that for a second it does render the other components as if it was getting the data and rendering but then it updates and they disappear. Please if someone can help me with this problem and explain to me why this happens it would be great.

App.js code

import React, {useState, useEffect} from 'react';
import Login from './components/Login';
import Player from './components/Player';
import { getTokenFromResponse } from './spotify';
import './styles/App.scss';
import SpotifyWebApi from "spotify-web-api-js";
import { library } from '@fortawesome/fontawesome-svg-core';
import { fas } from '@fortawesome/free-solid-svg-icons';
import { fab } from '@fortawesome/free-brands-svg-icons';
import { useDataLayer } from './components/Hooks/useDataLayer';

library.add(fas, fab);


//spotify library instance
const spotify = new SpotifyWebApi();

function App() {

  const [token, setToken] = useState(null);
  const [{ user }, dispatch] = useDataLayer();

  // where I get the necessary data from the api
  useEffect(() => {
    // function to get access token
    let accessToken = getTokenFromResponse();
    window.location.hash = '';

    if(accessToken){
      spotify.setAccessToken(accessToken);
      setToken(accessToken);

      //FROM HERE I GET THE DATA I NEED
      
      // here I get the data of my user
      spotify.getMe().then((data) =>{
        dispatch({
          type: "GET_USER",
          user: date
        })
      });
      
      spotify.getUserPlaylists().then((data) => {
        dispatch({
            type: "GET_PLAYLISTS",
            playlists: dates
        })
      });
      
      spotify.getMyTopTracks({limit: 4}).then((data) => {
        dispatch({
          type: "GET_TOP_TRACKS",
          top_tracks:data,
        })
      });

      spotify.getMyRecentlyPlayedTracks({limit: 4}).then((data) => {
        dispatch({
            type: "RECENTLY_PLAYED",
            recently_played: date,
        })
      });

      spotify.getMyTopArtists({limit: 4}).then((data) => {
        dispatch({
          type: "GET_TOP_ARTISTS",
          top_artists: date,
        })
      });
    }
  }, [token])

  //if the token is valid enter Player.js where Body.js is inside and if not return to the login component
  return (
    
      <div className="App">
        { token ? <Player spotify= {spotify} /> : <Login />}
      </div>
  );
}

export defaultApp;

Body.js code

import React from 'react'
import '../styles/Body.scss'
import { useDataLayer } from "./Hooks/useDataLayer.js";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

function Body({ spotify }) {

    //get the properties of the necessary data that I want to display in this component with useDataLayer
    const [{ spotify_recommendations, recently_played, top_tracks, top_artists }, dispatch] = useDataLayer();
   
    return (
        <div className= "main-body">
            <div className= "body-option">
                <span className= "see-more">See All</span>
                <FontAwesomeIcon icon={['fas', 'arrow-right']} />
                <div>
                    {
                        //to show the image and info of the track
                        recently_played?.items.map((item, index) =>{
                            return (
                                <div className= "track" key= {index}>
                                    <img src= {item.track.album.images[1].url} alt= "recently played track"></img>
                                    <div className= "track-data">
                                        <h3>{item.track.name}</h3>
                                        <p>{item.track.artists.map(artist => artist.name).join(", ")}</p>
                                    </div>
                                    
                                </div>
                            )
                        })
                        
                    }
                </div>
            </div>
            <div className= "body-option">
                <span className= "see-more">See All</span>
                <FontAwesomeIcon icon={['fas', 'arrow-right']} />
                <div>
                    {
                        //to show the image and info of the track
                       top_tracks?.items.map((topArtist, index) => {
                            return (
                                <div className= "track" key= {index}>
                                    <img src= {topArtist.album.images[1].url} alt= "recently played track"></img>
                                    <div className= "track-data">
                                        <h3>{topArtist.name}</h3>
                                        <p>{topArtist.artists.map(artist => artist.name).join(", ")}</p>
                                    </div>
                                    
                                </div>
                            )
                        })
                    }
                </div>
            </div>
            <div className= "body-option">
                <span className= "see-more">See All</span>
                <FontAwesomeIcon icon={['fas', 'arrow-right']} />
                <div>
                    {
                        //to show the image and info of the artist
                         top_artists?.items.map((topTrack, index) => {
                            return (
                                <div className= "track" key= {index}>
                                    <img src= {topTrack.images[1].url} alt= "recently played track"></img>
                                    <div className= "track-data">
                                        <h3>{topTrack.name}</h3>
                                        <p>{topTrack.genres.join(", ")}</p>
                                    </div>
                                    
                                </div>
                            )
                        })
                    }
                </div>
            </div>
        </div>
    )
}

export default Body

Code of my custom hook useDataLayer.js

import React, {useContext, createContext, useReducer} from 'react'

let DataContext = createContext();

export function DataLayer({reducer, initialState, children}) {
    return (
        <DataContext.Provider value= {useReducer(reducer, initialState)}>
            {children}
        </DataContext.Provider>
    )
}

export let useDataLayer = () => useContext(DataContext);

SideBar.js: the component where i display the user's playlist

import React from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useDataLayer } from './Hooks/useDataLayer.js';
import '../styles/SideBar.scss';




function SideBar({ spotify }){

    const [{ playlists }, dispatch] = useDataLayer();
    
    return (
        <div className= "side-bar">
            <div className= "side-bar-options">
                <div className= "spotify-logo">
                    <FontAwesomeIcon icon={['fab', 'spotify']} size= "3x"/>
                    <h1 className= "spotify-title">Spotify</h1>
                </div>
                <a className= "option" href= "./Player.js">
                    <FontAwesomeIcon icon={['fas', 'home']} />
                    <p className= "option-title" >Inicio</p>
                </a>
                <a className= "option" href= "./Player.js">
                    <FontAwesomeIcon icon={['fas', 'search']} />
                    <p className= "option-title">Buscar</p>
                </a>
                <a className= "option" href= "./Player.js">
                    <FontAwesomeIcon icon={['fas', 'headphones']} />
                    <p className= "option-title" >Tu Biblioteca</p>
                </a>
            </div>
            <p className= "playlist-title">PLAYLISTS</p>
            
            <div className= "playlists">
                {
                    playlists?.items?.map(
                        (list, index) => 
                        <p className="playlists-option" 
                            key={index}
                        >
                            {list.name}
                        </p>
                    ) 
                }
            </div>
        </div>
    )
}

export default SideBar;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

Player.js

import React from 'react';
import "../styles/Player.scss";
import SideBar from "./SideBar.js";
import Body from "./Body.js";

function Player({spotify}) {
  return (
  
  <div className= "player-container">
    <div className= "player_body">
        <SideBar  spotify= {spotify} />
        <Body  spotify= {spotify} />   
    </div>
  </div>

  )
}

export default Player;

spotify.js: the code where i get the token from the URL

const authEndpoint = "https://accounts.spotify.com/authorize";
const clientId = '84c134b175474ddabeef0e0b3f9cb389'
const redirectUri = 'http://localhost:3000/'
const scopes = [
    "user-read-currently-playing",
    "user-read-recently-played",
    "user-read-playback-state",
    "user-top-read",
    "user-modify-playback-state",
    "user-follow-modify",
    "user-follow-read",
    "playlist-modify-public",
    "playlist-modify-private",
    "playlist-read-private",
    "playlist-read-collaborative"
  ];

  //obtain acces token from url
  export const getTokenFromResponse = () => {
    let params = new URLSearchParams(window.location.hash.substring(1));
    let token = params.get("access_token");
    return token;
  };

  //acces url
const accessUrl = `${authEndpoint}?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scopes.join("%20")}&response_type=token&show_dialog=true`;

export default accessUrl;

Thank you very much for your time and attention.

FERRE FACU
  • 11
  • 3

1 Answers1

0

By the look of it, your useEffect hook is being called twice in quick succession. It has the dependency of token, which starts as null for your first render. Inside the hook you then read the accessToken and set the state for it to your token. Doing this will trigger another render because the dependency changed from null to the value of your access token.

I would suggest to simply remove token from your useEffect dependency array (so that your hook acts as an "on mounted" event) and see if that gets you the desired effect. You should eventually move the getTokenFromResponse outside the hook, assuming it is not available on first render, as I'm just guessing it is immediately available.

Post comments, it might be better to separate your initializing code into a useEffect that only runs once, when the component is first mounted. This way, as I now suspect getTokenFromResponse() returns a new Object every call, you only need to call it once. Note, if it returns a Promise, this won't work, so you should verify this first before trying.

To better illustrate my point, I've put it in code with some comments:

  useEffect(() => {
    setToken(getTokenFromResponse());

    // this only needs to happen once
    window.location.hash = '';
  }, []; // <- empty dependency array will only run once, on mount
  
  useEffect(() => {
    // out of the 2 times this will be called,
    //   it should only pass this condition on the last/2nd
    if(token){
      spotify.setAccessToken(token);

      //FROM HERE I GET THE DATA I NEED
      
      // here I get the data of my user
      spotify.getMe().then((data) =>{
        dispatch({
          type: "GET_USER",
          user: date
        })
      });
      
      spotify.getUserPlaylists().then((data) => {
        dispatch({
            type: "GET_PLAYLISTS",
            playlists: dates
        })
      });
      
      spotify.getMyTopTracks({limit: 4}).then((data) => {
        dispatch({
          type: "GET_TOP_TRACKS",
          top_tracks:data,
        })
      });

      spotify.getMyRecentlyPlayedTracks({limit: 4}).then((data) => {
        dispatch({
            type: "RECENTLY_PLAYED",
            recently_played: date,
        })
      });

      spotify.getMyTopArtists({limit: 4}).then((data) => {
        dispatch({
          type: "GET_TOP_ARTISTS",
          top_artists: date,
        })
      });
    }
    
    // this will trigger the effect any time token changes, 
    //   which should now only be twice:
    //   1st at it's default state value (null)
    //   2nd after the previous useEffect sets its state 
  }, [token]); 

Hope that helps but, if it's a promise, you'll need to handle it according (suggestion: moving the setToken into the then)

Good luck!

Steve Hynding
  • 1,799
  • 1
  • 12
  • 22
  • Hello Steve, I just tried that and it didn't work since it still doesn't load the three sections and not only that but I have another component where the user's playlists appear that used to work and now it also renders and then disappears quickly. I don't know what else to do – FERRE FACU May 08 '22 at 01:57
  • From what I understand, the useEffect is running 5 times, one for each time I call the Spotify API, or rather, I make a change to my page. So I have the theory that for each time the useEffect is executed, it obtains the data of one section at a time and that is why it is not saving the data obtained the previous times that it was executed in the initialState of the reducer. But I really don't know how to fix it. – FERRE FACU May 08 '22 at 01:59
  • Let's dig deeper. I do know `useEffect` will be called any time it's dependencies change (2nd parameter list as an array, i.e. `[token]`. If it's not a string but an object it may be the reason. The object may be getting created from scratch every time which is not good for useEffect as it only tracks the pointer to that object (which stays the same even though the properties within may change. I'll update my answer now with a better suggestion. – Steve Hynding May 08 '22 at 03:58
  • It still doesn't work and now it's worse because I had a component called SideBar.js where I got the data from the user's playlists that used to work and displayed fine and now the same thing happens to me as with the Body.js sections I am going to edit and add the SideBar.js code but I also think that the problem lies in the useEfect – FERRE FACU May 08 '22 at 19:03
  • I feel I'm missing some of the additional context going along with this that is causing the errors. If you're certain it's the useEffect, try adding in console.log checks or breakpoints to determine how often it is getting called. I also see the Body component but not one for the Player. The getTokenFromResponse source might be useful to see what is going on there. – Steve Hynding May 08 '22 at 22:57
  • The Player component is a functional component that has the SideBaR.JS and Body.js components inside. I didn't think it was important to add it but now I add the code of the things you said – FERRE FACU May 09 '22 at 01:28