skip to Main Content

I’m fetching 3 different endpoints from my backend. My goal is to display all the data. I need to pass this data to the correct components, however I am using dynamic routes.

My error is that my data comes to artists.js as an empty array.

I have tried:

Using useNavigate to pass the data link to previous SOF question, user suggested redux toolkit

Using Link to pass the data.

Using redux toolkit to pass the data. I am new to Redux, so it’s possible I have done something wrong in my code.

I believe it’s a timing, rendering cycle error. However I am not sure how to fix it. When I put in some conditional rendering in the artists component, it showed me the correct data in the console logs on it’s second render. But now I am not sure what to do from there. It is stuck with the page saying "loading". I tried mapping the data after the conditional rendering (where return <div>Loading...</div>; is), but on page refresh, all the rendered data disappears from the page.

here is the code from top to bottom:

index.js

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { BrowserRouter } from "react-router-dom";
import { Provider } from "react-redux";
import store from "./redux/store";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </Provider>
  </React.StrictMode>
);

reportWebVitals();

app.js

import "./App.css";
import Genres from "./Components/Genres"
import TopTracks from "./Components/TopTracks"
import Results from "./Components/Results"
import { Routes, Route} from "react-router-dom"

function App() {
  return (
    <Routes>
      <Route path='/' element={<Genres />} />
      <Route path='/:genre' element={<Results />} />
      <Route path='/:artist' element={<TopTracks />} />
    </Routes>
  );
}

export default App;

genres.js

import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { useDispatch } from 'react-redux';
import { setData } from '../redux/spotifyDataSlice';

function Genres() {
  const [genres, setGenres] = useState([]);
  const dispatch = useDispatch();

  useEffect(() => {
    fetch("/api/genres/")
      .then((response) => response.json())
      .then((data) => setGenres(data.genres))
      .catch((error) => console.log(error));
  }, []);

  function handleClick(genre) {
    const query_params = {
      genre: genre
    };
  
    Promise.all([
      fetch("/api/artists/", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "X-CSRFToken": "",
        },
        body: JSON.stringify(query_params),
      }),
      fetch("/api/playlists/", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "X-CSRFToken": "",
        },
        body: JSON.stringify(query_params),
      }),
      fetch("/api/tracks/", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "X-CSRFToken": "",
        },
        body: JSON.stringify(query_params),
      })
    ])
      .then((responses) => Promise.all(responses.map((response) => response.json())))
      .then(([artists, playlists, tracks]) => {
        dispatch(setData({ artists, playlists, tracks }));
      })
      .catch((error) => console.log(error));
  }

  return (
    <div>
      <div className="genre-list-container">
        <ul className="genre-list">
          {genres.map((genre) => (
            <li className="genre" key={genre}>
              <Link to={`/${genre}`} onClick={() => handleClick(genre)}>{genre}</Link>
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
}

export default Genres;

results.js

import React from "react";
import Artists from "./Artists";
import Playlists from "./Playlists";
import Tracks from "./Tracks";

function Results() {
  return (
    <div>
      <Artists />
      <Playlists />
      <Tracks />
    </div>
  );
}
export default Results;

artists.js

import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useSelector } from 'react-redux';

function Artists() {
  const navigate = useNavigate();
  const artists = useSelector((state) => state.spotifyData.artists);
  console.log(artists)
  const [isLoading, setIsLoading] = useState(true);

  if (isLoading) {
    if (artists.length === 0) {
      return <div>No artists found</div>;
    } else {
      return <div>Loading...</div>;
    }
  }

  function handleClick(artist) {
    fetch("/api/top_tracks/", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-CSRFToken": "",
      },
      body: JSON.stringify({ artist_id: artist.id }),
    })
      .then((response) => response.json())
      .then((data) => {
        console.log(data);
        navigate(`/${artist.name}`, { state: { data } });
      });
  }

  return (
    <div>
      <div>Artists:</div>
      {artists.map((artist) => (
        <div key={artist.id}>
          <img src={artist.image_url} alt={artist.name} />
          <h1 onClick={() => handleClick(artist)}>{artist.name}</h1>
          <p>Popularity: {artist.popularity}</p>
          <p>Genres: {artist.genres}</p>
        </div>
      ))}
    </div>
  );
}
export default Artists;

spotifyDataSlice.js:

import { createSlice } from '@reduxjs/toolkit';

const spotifyDataSlice = createSlice({
  name: 'spotifyData',
  initialState: {
    artists: [],
    playlists: [],
    tracks: [],
  },
  reducers: {
    setData(state, action) {
      const { artists, playlists, tracks } = action.payload;
      console.log("Artists:", artists);
      console.log("Playlists:", playlists);
      console.log("Tracks:", tracks);
      state.artists = artists;
      state.playlists = playlists;
      state.tracks = tracks;
    },
  },
});

export const { setData } = spotifyDataSlice.actions;
export default spotifyDataSlice.reducer;

store.js:

import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';
import spotifyDataReducer from './spotifyDataSlice';

const loggerMiddleware = store => next => action => {
  console.log('Dispatching:', action);
  const result = next(action);
  console.log('Next State:', store.getState());
  return result;
};

const store = configureStore({
  reducer: {
    spotifyData: spotifyDataReducer,
  },
  middleware: getDefaultMiddleware().concat(loggerMiddleware)
});

export default store;

edit:
Here are some examples of my POST responses.

{artists: {…}, playlists: {…}, tracks: {…}}

artists:

artists: {total: 13, artists: Array(13)}

example of artists object:

{name: 'Aborted', id: '1XRhUgCyzIdeT8d9KMfeDR', image_url: 'https://i.scdn.co/image/ab6761610000e5eb457a41afcb20ec257fce22d4', popularity: 39, genres: Array(8)}

2

Answers


  1. Instead of trying to manage any data loading state from the Results or children pages, I’d recommend actually leveraging redux-toolkit more by moving all the fetching logic into an asynchronous action that can have its loading status incorporated right into the state.

    Example:

    spotifyDataSlice.js

    Create a Thunk function to handle the fetching, and update the state slice to include a loading state that is updated when the asynchronous action is pending and settles.

    import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
    
    export const fetchData = createAsyncThunk(
      'spotifyData/fetchData',
      async (genre, thunkAPI) => {
        const options = {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "X-CSRFToken": "",
          },
          body: JSON.stringify({ genre }),
        };
      
        try {
          const requests = ["artists", "playlists", "tracks"]
            .map(key => fetch(`/api/${key}/`, options)
              .then(response => response.json())
              // access response property, e.g. data.artists, data.playlists, etc
              .then(data => data[key])
            );
    
          const [artists, playlists, tracks] = await Promise.all(requests);
          return { artists, playlists, tracks };
        } catch(error) {
          return thunkAPI.rejectWithValue(error);
        }
        
      }
    );
    
    const spotifyDataSlice = createSlice({
      name: 'spotifyData',
      initialState: {
        loading: false,
        artists: [],
        playlists: [],
        tracks: [],
      },
      extraReducers: builder => {
        builder
          .addCase(fetchData.pending, (state) => {
            state.loading = true;
          })
          .addCase(fetchData.fulfilled, (state, action) => {
            state.loading = false;
            state.artists = action.payload.artists;
            state.playlists = action.payload.playlists;
            state.tracks = action.payload.tracks;
          })
          .addCase(fetchData.rejected, (state) => {
            state.loading = false;
          });
        },
      },
    });
    
    export default spotifyDataSlice.reducer;
    

    genres.jsx

    import React, { useState, useEffect } from "react";
    import { Link } from "react-router-dom";
    import { useDispatch } from 'react-redux';
    import { fetchData } from '../redux/spotifyDataSlice';
    
    function Genres() {
      const dispatch = useDispatch();
    
      const [genres, setGenres] = useState([]);
    
      useEffect(() => {
        fetch("/api/genres/")
          .then((response) => response.json())
          .then((data) => setGenres(data.genres))
          .catch((error) => console.log(error));
      }, []);
    
      const handleClick = () => genre => {
        dispatch(fetchData(genre));
      }
    
      return (
        <div className="genre-list-container">
          <ul className="genre-list">
            {genres.map((genre) => (
              <li className="genre" key={genre}>
                <Link to={`/${genre}`} onClick={handleClick(genre)}>
                  {genre}
                </Link>
              </li>
            ))}
          </ul>
        </div>
      );
    }
    
    export default Genres;
    

    results.jsx

    import React from "react";
    import { useSelector } from 'react-redux';
    import Artists from "./Artists";
    import Playlists from "./Playlists";
    import Tracks from "./Tracks";
    
    function Results() {
      const isLoading = useSelector(state => state.spotifyData.loading);
    
      return (
        <div>
          {isLoading
            ? <div>Loading...</div>
            : (
              <>
                <Artists />
                <Playlists />
                <Tracks />
              </>
            )
          }
        </div>
      );
    }
    
    export default Results;
    

    artists.jsx (Playlists and Tracks components get same treatment)

    import React, { useState } from "react";
    import { useNavigate } from "react-router-dom";
    import { useSelector } from 'react-redux';
    
    function Artists() {
      const navigate = useNavigate();
      const artists = useSelector(state => state.spotifyData.artists);
    
      function handleClick(artist) {
        ....
      }
    
      if (!artists.length) {
        return <div>No artists found</div>;
      }
    
      return (
        <div>
          <div>Artists:</div>
          {artists.map((artist) => (
            <div key={artist.id}>
              <img src={artist.image_url} alt={artist.name} />
              <h1 onClick={() => handleClick(artist)}>{artist.name}</h1>
              <p>Popularity: {artist.popularity}</p>
              <p>Genres: {artist.genres}</p>
            </div>
          ))}
        </div>
      );
    }
    
    export default Artists;
    

    but on page refresh, all the rendered data disappears from the page

    Redux is in-memory state management, when the page is reloaded, the memory from the previous instance is dumped. The typical solution is to persist your redux store to localStorage or anything more longterm. Redux Persist is a great common solution for this and is easy to integrate.

    As a stop-gap you could simply move the fetching logic to the "/:genres" route so that when the page is reloaded and Results is mounted it will simply trigger a refetch.

    import React, { useState, useEffect } from "react";
    import { Link } from "react-router-dom";
    
    function Genres() {
      const [genres, setGenres] = useState([]);
    
      useEffect(() => {
        fetch("/api/genres/")
          .then((response) => response.json())
          .then((data) => setGenres(data.genres))
          .catch((error) => console.log(error));
      }, []);
    
      return (
        <div className="genre-list-container">
          <ul className="genre-list">
            {genres.map((genre) => (
              <li className="genre" key={genre}>
                <Link to={`/${genre}`}>
                  {genre}
                </Link>
              </li>
            ))}
          </ul>
        </div>
      );
    }
    
    export default Genres;
    
    import React from "react";
    import { useDispatch, useSelector } from 'react-redux';
    import { useParams } from "react-router-dom";
    import Artists from "./Artists";
    import Playlists from "./Playlists";
    import Tracks from "./Tracks";
    import { fetchData } from '../redux/spotifyDataSlice';
    
    function Results() {
      const dispatch = useDispatch();
      const { genre } = useParams();
    
      const isLoading = useSelector(state => state.spotifyData.loading);
    
      React.useEffect(() => {
        // fetch data on component mount or when genre value changes
        dispatch(fetchData(genre));
      }, [dispatch, genre]);
    
      return (
        <div>
          {isLoading
            ? <div>Loading...</div>
            : (
              <>
                <Artists />
                <Playlists />
                <Tracks />
              </>
            )
          }
        </div>
      );
    }
    
    export default Results;
    
    Login or Signup to reply.
  2. The better approach is to refactor your code as the answer below (I also have bookmarked it).

    However, I will answer your unexpected behavior:

    …it showed me the correct data in the console logs on it’s second render. But now I am not sure what to do from there. It is stuck with the page saying "loading".

    The reason it seem "stuck" at "Loading" is that inside artists.js you are never updating the isLoading state to false, so isLoading is always true.

    The issue with the condition:

    if (isLoading) {
        if (artists.length === 0) {
          return <div>No artists found</div>;
        } else {
          return <div>Loading...</div>;
        }
      }
    

    Is saying: if isLoading is true then check if artists.length === 0: on the initial render it is true so it correctly shows the message "No artists found", but the issue is that isLoading is always true and when artists‘s length is not 0 meaning it contains at least 1 item, the condition says: isLoading is true and artists.length is not 0: so it correctly returns "Loading" (even when there’s data inside artists).

    But as-is in your case you may not want to return "Loading" in artists.js since the condition is ‘artists‘s length’ which on initial render (when component mounts) has a length of 0 -> is an empty array ([]).

    However, the "Loading" could still be used when making a fetch call like your handleClick does (but also parent component Results will use the "Loading" if you set up the code like in the below answer, which is a far better choice), so the final code of artists.js would look like:

    import React, { useState } from "react";
    import { useNavigate } from "react-router-dom";
    import { useSelector } from 'react-redux';
    
    function Artists() {
      const navigate = useNavigate();
      const artists = useSelector((state) => state.spotifyData.artists);
      console.log(artists)
      const [isLoading, setIsLoading] = useState(false); // modified initial state: false
    
      function handleClick(artist) {
        setIsLoading(true); // start Loading before the fetch call
        fetch("/api/top_tracks/", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "X-CSRFToken": "",
          },
          body: JSON.stringify({ artist_id: artist.id }),
        })
          .then((response) => response.json())
          .then((data) => {
            console.log(data);
            navigate(`/${artist.name}`, { state: { data } });
            setIsLoading(false); // stop Loading once data is received
          });
      }
    
      if (isLoading) {
       return <div>Loading...</div>
      }
    
      return (
        <div>
         {artists.length === 0 ? <div>No artists found</div> : (
         <>
          <div>Artists:</div>
          {artists.map((artist) => (
            <div key={artist.id}>
              <img src={artist.image_url} alt={artist.name} />
              <h1 onClick={() => handleClick(artist)}>{artist.name}</h1>
              <p>Popularity: {artist.popularity}</p>
              <p>Genres: {artist.genres}</p>
            </div>
          ))}
         </>
         )
        </div>
      );
    }
    export default Artists;
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search