Component didn't recieve data from state using React/Toolkit and createAsyncThunk

Viewed 23

I'm trying to pass data to the component, that i received from the API. I am using 'createAsyncThunk' to save it in the state, than when trying to get my data, get error "undefined". I understand that it happens, cause it's need some time to get data from API, but how i can force component "waiting"? What is wrong with my code?

Here is my code:

Step 1: Gettind data from API, filtered it and push it in state.

import { generateRandom } from "../helpers/randomInt";

const API_URL = "https://akabab.github.io/superhero-api/api/all.json";

export const fetchHeroes = createAsyncThunk(
  "data_slice/fetchHeroes",
  async function (_, { rejectWithValue }) {
    try {
      const res = await fetch(API_URL);
      if (!res.ok) {
        throw new Error("Could not fetch cart data!");
      }
      const data = await res.json();

      const marvel_heroes = data.filter(
        (item) => item.biography.publisher == "Marvel Comics"
      );
      const dark_horse_heroes = data.filter(
        (item) => item.biography.publisher == "Dark Horse Comics"
      );
      const dc_heroes = data.filter(
        (item) => item.biography.publisher == "DC Comics"
      );

      const filtered_data = [
        ...marvel_heroes,
        ...dark_horse_heroes,
        ...dc_heroes,
      ];

      const heroesData = [];

      for (let index = 0; index < 49; index++) {
        const item = filtered_data[generateRandom(0, 439)];
        heroesData.push(item);
      }

      const main_data = [filtered_data, heroesData];
      return main_data;
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);

const heroesSlice = createSlice({
  name: "data_slice",
  initialState: { heroes_data: [], isLoading: null, error: null },

  extraReducers: {
    [fetchHeroes.pending]: (state) => {
      state.isLoading = true;
    },
    [fetchHeroes.fulfilled]: (state, action) => {
      state.heroes_data = action.payload;
      state.isLoading = false;
    },
    [fetchHeroes.rejected]: (state) => {
      state.isLoading = false;
      state.error = "Something go wrong!";
      alert("aaa");
    },
  },
});

export default heroesSlice;

Step 2: Firing (using dispatch) fetch function "fetchHeroes" in 'App.js' with 'UseEffect' to get data when app starting

import { Fragment, useState, useEffect } from "react";

import { useSelector, useDispatch } from "react-redux";
import { fetchHeroes } from "./store/heroes-slice";

import { Routes, Route } from "react-router-dom";

import Main from "./pages/Main";
import Heroes from "./pages/Heroes";
import Hero_page from "./pages/Hero_page";

import LoginModal from "./components/LoginModal";
import RegisterModal from "./components/RegisterModal";

function App() {
  const [scrollY, setScrollY] = useState(0);

  const isLoginModal = useSelector((state) => state.modal.isLoginModal);
  const isRegisterModal = useSelector((state) => state.modal.isRegisterModal);

  const dispatch = useDispatch();

  function logit() {
    setScrollY(window.scrollY);
    console.log(new Date().getTime());
  }

  useEffect(() => {
    function watchScroll() {
      window.addEventListener("scroll", logit);
    }
    watchScroll();
    return () => {
      window.removeEventListener("scroll", logit);
    };
  });

  useEffect(() => {
    dispatch(fetchHeroes());
  }, [dispatch]);

  return (
    <Fragment>
      {isRegisterModal && <RegisterModal></RegisterModal>}
      {isLoginModal && <LoginModal></LoginModal>}
      <Routes>
        <Route path="/" element={<Main />} />
        <Route path="/heroes" exact element={<Heroes scroll={scrollY} />} />
        <Route path="/heroes/:heroId" element={<Hero_page />}></Route>
      </Routes>
    </Fragment>
  );
}

export default App;

Step 3: I am trying to recieve data from state(heroes_fetched_data) using 'usSelector', but when trying parce it through 'map', get error 'undefined'

import classes from "./Heroes.module.css";
import Header from "../components/Header";
import Footer from "../components/Footer.js";
import Hero_card from "../components/Hero_card";
import { useSelector } from "react-redux";
import { Link } from "react-router-dom";

export default function Heroes(props) {
  const heroes_fetched_data = useSelector((state) => state.heroes.heroes_data);
  const loadingStatus = useSelector((state) => state.heroes.isLoading);

  console.log(heroes_fetched_data);

  const heroes_cards = heroes_fetched_data[1].map((item, i) => (
    <Link to={`/heroes/${item.id}`} key={item.id + Math.random()}>
      <Hero_card
        key={i}
        img={item.images.lg}
        name={item.name}
        publisher={item.biography.publisher}
      />
    </Link>
  ));

  return (
    <div className={classes.main}>
      <Header scroll={props.scroll} />
      {!loadingStatus && (
        <section className={classes.heroes}>
          <ul className={classes.ully} id="heroes">
            {heroes_cards}
          </ul>
        </section>
      )}

      {loadingStatus && <p>Loading...</p>}
      <Footer />
    </div>
  );
}

1 Answers

Because the fetch is asynchronous, you cannot assume that heroes_fetched_data inside your Heroes component will have the data when the component first renders. You need to check whether this data is present before attempting to use it. If it's not yet present, the component should render an alternate "loading" state. When the fetch completes, your component should re-render automatically, at which point heroes_fetched_data will have the data you want and you can proceed.

Roughly, you want something like this pseudocode:

export default function Heroes(props) {
  const heroes_fetched_data = useSelector((state) => state.heroes.heroes_data);
  const loadingStatus = useSelector((state) => state.heroes.isLoading);

  if (!heroes_fetched_data) {
    return <p>{loadingStatus}</p>;
  }

  const heroes_cards = heroes_fetched_data[1].map((item, i) => (
    // ...
  );

  // proceed as normal
}
Related