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
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.
genres.jsx
results.jsx
artists.jsx (
Playlists
andTracks
components get same treatment)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 andResults
is mounted it will simply trigger a refetch.The better approach is to refactor your code as the answer below (I also have bookmarked it).
However, I will answer your unexpected behavior:
The reason it seem "stuck" at "Loading" is that inside artists.js you are never updating the
isLoading
state tofalse
, soisLoading
is alwaystrue
.The issue with the condition:
Is saying: if
isLoading
istrue
then check ifartists.length === 0
: on the initial render it istrue
so it correctly shows the message "No artists found", but the issue is thatisLoading
is alwaystrue
and whenartists
‘s length is not0
meaning it contains at least 1 item, the condition says:isLoading
istrue
andartists.length
is not0
: so it correctly returns "Loading" (even when there’s data insideartists
).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 of0
-> is an empty array ([]
).However, the "Loading" could still be used when making a fetch call like your
handleClick
does (but also parent componentResults
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: