State is null, after useEffect hooks

Viewed 1591

I am trying to set product by dispatching a method in a useEffect. However, state still says null.

index.html

import React, { useEffect, Fragment } from "react";
import { useSelector, useDispatch } from "react-redux";

import { fetchProductsData } from "../../store/products-actions";

import Promotion from "./components/Promotion";
import Products from "./components/Products";
import ToastUi from "../../shared/ui/ToastUi";

import { Container, Row, Col } from "react-bootstrap";

const Home = () => {
  const dispatch = useDispatch();

  // const products = useSelector((state) => state.products.products);
  const products = useSelector((state) => state.products.productsTest);
  const cartQuantity = useSelector((state) => state.cart.quantity);

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

  return (
    <Fragment>
      <ToastUi
        status="Sukses"
        title="Notifikasi"
        message={`(${cartQuantity}) produk baru berhasil di masukkan keranjang`}
      />
      <Container fluid="xl">
        <Row>
          <Col>
            <Promotion />
          </Col>
        </Row>
        <Row className="mt-3" md={3}>
          <Products products={products} />
        </Row>
      </Container>
    </Fragment>
  );
};

export default Home;

Products still says null after a cycle, apparently it needs second cycle to make that state changed. Not sure how can I make it change in one cycle. Do I need to put the useEffect in the parent ?

EDIT if I add this, it will work

 {products !== null && <Products products={products} />}
 // {/* <Products products={products} /> */} // 

However, is there a better way or maybe some explanation on why this is happening, Thank you.

EDIT

products-slice.js

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

import {
  products,
  excel_products,
  product,
  filteredProducts,
  productsTest,
} from "../datafiles";

const initialProductsState = {
  products,
  excel_products,
  product,
  filteredProducts,
  productsTest,
};

const productsSlice = createSlice({
  name: "products",
  initialState: initialProductsState,
  reducers: {
    viewExcelProducts(state, action) {
      state.excel_products = action.payload;
    },
    uploadExcelProducts(state) {
      if (excel_products.length < 0) {
        console.log("error");
      } else {
        const newProducts = state.products.concat(state.excel_products);
        state.products = newProducts;
        state.excel_products = [];
      }
    },
    selectProduct(state, action) {
      const product = state.products.find((item) => item.id === action.payload);
      state.product = product;
    },
    filterProducts(state, action) {
      const filteredProducts = state.products.filter(
        (item) => item.type === action.payload
      );
      state.filteredProducts = filteredProducts;
    },
    setProducts(state, action) {
      state.productsTest = action.payload;
    },
  },
});

export const productsActions = productsSlice.actions;
export default productsSlice;

products-actions.js

import { productsActions } from "./products-slice";

export const fetchProductsData = () => {
  return async (dispatch) => {
    const fetchData = async () => {
      const response = await fetch("http://localhost:5000/products");

      if (!response.ok) {
        throw new Error("Could not fetch data!");
      }
      const data = await response.json();

      return data;
    };
    try {
      const productsData = await fetchData();
      dispatch(productsActions.setProducts(productsData));
    } catch (err) {
      console.log("Error: " + err);
    }
  };
};
1 Answers

What do you mean by it needs second cycle to make that state changed?

fetchProductsData is an async function, I assume. That means that you do not receive data immediately, but after some time (depending on network connection speed, payload size etc). So it is OK that your data arrives later.

Usual approach for async data is to keep isLoading in your state. And use it as following:

  const isLoading = useSelector((state) => state.products.isLoading);
  ...
  return (
    <Fragment>
      ...
      {isLoading && <Spinner />} // Some loading indicator
      {!isLoading && <Products products={products} />}
    </Fragment>
  );

This way you will indicate to user that some data is being fetched. This is a good UX approach.

isLoading should be set somewhere in your fetchProductsData action, like so:

export const fetchProductsData = () => {
  return async (dispatch) => {
    ...
    try {
      dispatch(productsActions.setIsLoading(true));
      const productsData = await fetchData();
      dispatch(productsActions.setProducts(productsData));
    } catch (err) {
      console.log("Error: " + err);
    } finally {
      dispatch(productsActions.setIsLoading(false));
    }
  };
};

Related