Search code examples
reactjsredux

React + Redux: Data not being passed correctly components with dynamic routes


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)}

Solution

  • 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;