Search code examples
reduxreact-reduxredux-toolkit

Data fetched from createAsyncThunk is undefined


I want to display all the books in the file db.json located in the server folder when the browser is run the first time. I'm using json-server to do that. However, I caught the error Uncaught TypeError: Cannot read properties of undefined (reading 'books') in the HomePage.js. I tried to look at the bookListSlice.js and its fetchBookData function, but I couldn't figure out what I missed. How can I overcome that problem?

Here is the full codebase: https://github.com/PhucDong/Redux-Bookstore

Here are the codes in HomePage.js

import { useState, useEffect } from "react";
import { ClipLoader } from "react-spinners";
import { useNavigate } from "react-router-dom";
import PaginationBar from "../components/PaginationBar";
import SearchForm from "../components/SearchForm";
import { FormProvider } from "../form";
import { useForm } from "react-hook-form";
import {
  Container,
  Alert,
  Box,
  Card,
  Stack,
  CardMedia,
  CardActionArea,
  Typography,
  CardContent,
} from "@mui/material";
import { BACKEND_API } from "../config";
import { useDispatch, useSelector } from "react-redux";
import {
  fetchBookData,
} from "../service/bookList/bookListSlice";

const HomePage = () => {
  const [pageNum, setPageNum] = useState(1);
  const totalPage = 10;
  const limit = 10;

  const [loading, setLoading] = useState(false);
  const [query, setQuery] = useState("");
  const [errorMessage, setErrorMessage] = useState("");

  const navigate = useNavigate();
  const handleClickBook = (bookId) => {
    navigate(`/books/${bookId}`);
  };

  const bookData = useSelector((state) => state.bookList.books);
  const dispatch = useDispatch();

  useEffect(() => {
    setLoading(true);
    let url = `/books?_page=${pageNum}&_limit=${limit}`;
    if (query) {
      url += `&q=${query}`;
    }
    dispatch(fetchBookData(url));

    setLoading(false);
  }, [dispatch, pageNum, query]);

  //------React hook form
  const defaultValues = {
    searchQuery: "",
  };
  const methods = useForm({
    defaultValues,
  });
  const { handleSubmit } = methods;
  const onSubmit = (data) => {
    setQuery(data.searchQuery);
  };
  //------

  return (
    <Container>
      <Stack sx={{ display: "flex", alignItems: "center", m: "2rem" }}>
        <Typography variant="h3" sx={{ textAlign: "center" }}>
          Book Store
        </Typography>

        {errorMessage && <Alert severity="danger">{errorMessage}</Alert>}

        <FormProvider methods={methods} onSubmit={handleSubmit(onSubmit)}>
          <Stack
            spacing={2}
            direction={{ xs: "column", sm: "row" }}
            alignItems={{ sm: "center" }}
            justifyContent="space-between"
            mb={2}
          >
            <SearchForm />
          </Stack>
        </FormProvider>

        <PaginationBar
          pageNum={pageNum}
          setPageNum={setPageNum}
          totalPageNum={totalPage}
        />
      </Stack>

      <div>
        {loading ? (
          <Box sx={{ textAlign: "center", color: "primary.main" }}>
            <ClipLoader color="inherit" size={150} loading={true} />
          </Box>
        ) : (
          <Stack
            direction="row"
            spacing={2}
            justifyContent="space-around"
            flexWrap="wrap"
          >
            {bookData.map((book) => (
              <Card
                key={book.id}
                onClick={() => handleClickBook(book.id)}
                sx={{
                  width: "12rem",
                  height: "27rem",
                  marginBottom: "2rem",
                }}
              >
                <CardActionArea>
                  <CardMedia
                    component="img"
                    image={`${BACKEND_API}/${book.imageLink}`}
                    alt={`${book.title}`}
                  />
                  <CardContent>
                    <Typography gutterBottom variant="h5" component="div">
                      {`${book.title}`}
                    </Typography>
                  </CardContent>
                </CardActionArea>
              </Card>
            ))}
          </Stack>
        )}
      </div>
    </Container>
  );
};

export default HomePage;

Here are the codes in bookListSlice.js

import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import apiService from "../../apiService";

const initialState = {
  books: [],
  error: "",
};

export const fetchBookData = createAsyncThunk(
  "books/fetchBooks",
  async (url) => {
    const response = await apiService.get(url);
    return response.data;
  }
);

export const bookListSlice = createSlice({
  name: "bookList",
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchBookData.pending, (state, action) => {
        state.status = "loading";
      })
      .addCase(fetchBookData.fulfilled, (state, action) => {
        state.status = "succeeded";
        state.books = state.books.concat(action.payload);
      })
      .addCase(fetchBookData.rejected, (state, action) => {
        state.status = "failed";
        state.error = action.error.message;
      });
  },
});

export default bookListSlice.reducer;


Solution

  • I checked. The issue you facing its in store.js

    You have to call the reducer like that.

    import bookListSlice from "./bookList/bookListSlice";
    

    In better way you can call like that for not confusing

    import { configureStore } from "@reduxjs/toolkit";
    import bookListSliceReducer from "./bookList/bookListSlice";
    
    const store = configureStore({
        reducer: {
            bookList: bookListSliceReducer,
        },
    });
    
    export default store;
    

    Because you importing this

    enter image description here

    Not this

    enter image description here

    And this is how it's looking now.

    enter image description here

    So let me know if this is the problem you asking about.