Search code examples
javascriptreactjsredux-toolkit

cannot read properties of undefined (reading 'img') after updating the img value with redux toolkit


Hello so i have a project made using MERN and redux-toolkit(i use old version bcs tutorial), in the product page i want to update selected product. The update route runs without any errors. after updating i got an error cannot read properties of undefined (reading 'img') but the product data in the database and redux state is successfully changing. I need to go back to my homepage and reload my browser multiple times in order to remove the error. Can someone tell me whats wrong the error and i need explanation. Thanks.

Product State : id, title, desc, img, categories[], materials, price

ProductRedux :

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

export const productSlice = createSlice({
  name: "product",
  initialState: {
    products: [],
    isFetching: false,
    error: false,
  },
  reducers: {
    //GET ALL
    getProductStart: (state) => {
        state.isFetching = true;
        state.error = false;
    },
    getProductSuccess: (state, action) => {
        state.isFetching = false;
        state.products = action.payload;
    },
    getProductFailure: (state) => {
        state.isFetching = true;
        state.error = true;
    },
    //DELETE
    deleteProductStart: (state) => {
        state.isFetching = true;
        state.error = false;
    },
    deleteProductSuccess: (state, action) => {
        state.isFetching = false;
        state.products.splice(
            state.products.findIndex((item) => item._id === action.payload),1
        )
    },
    deleteProductFailure: (state) => {
        state.isFetching = true;
        state.error = true;
    },
    //UPDATE
    updateProductStart: (state) => {
        state.isFetching = true;
        state.error = false;
    },
    updateProductSuccess: (state, action) => {
        state.isFetching = false;
        state.products[state.products.findIndex((item) => item._id === action.payload.id)] = action.payload.product;
    },
    updateProductFailure: (state) => {
        state.isFetching = true;
        state.error = true;
    },
    //ADD
    addProductStart: (state) => {
        state.isFetching = true;
        state.error = false;
    },
    addProductSuccess: (state, action) => {
        state.isFetching = false;
        state.products.push(action.payload);
    },
    addProductFailure: (state) => {
        state.isFetching = true;
        state.error = true;
    },
  },
});

export const {
    getProductStart,
    getProductSuccess,
    getProductFailure,
    deleteProductStart,
    deleteProductSuccess,
    deleteProductFailure,
    updateProductStart,
    updateProductSuccess,
    updateProductFailure,
    addProductStart,
    addProductSuccess,
    addProductFailure
} = productSlice.actions;

export default productSlice.reducer;

store :

import { configureStore, combineReducers } from "@reduxjs/toolkit"
import userReducer from './userRedux';
import productReducer from './productRedux';
import {
    persistStore,
    persistReducer,
    FLUSH,
    REHYDRATE,
    PAUSE,
    PERSIST,
    PURGE,
    REGISTER,
  } from "redux-persist";
import storage from "redux-persist/lib/storage"

const persistConfig = {
  key: "root",
  version: 1,
  storage,
};

const rootReducer = combineReducers({ user: userReducer, product: productReducer });

const persistedReducer = persistReducer(persistConfig, rootReducer);

  
export const store = configureStore({
    reducer: persistedReducer,

    middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: {
        ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
      },
    }),
})

export let persistor = persistStore(store);

apiCall :

export const updateProduct = async (dispatch, product, id) => {
    dispatch(updateProductStart());
    try {
        const res = await userMethod.put(`/product/update/${id}`, product);
        dispatch(updateProductSuccess({ product, id }));
    } catch (err) {
        dispatch(updateProductFailure());
    }
}

Product Page :

export default function Product() {

  const location = useLocation();
  const productId = location.pathname.split("/")[2];
  const [ productStats, setProductStats ] = useState([]);
  const [ inputs, setInputs ] = useState({});
  const [ file, setFile ] = useState(null);
  const [ cat, setCat ] = useState([]);
  const dispatch = useDispatch();

  const product = useSelector((state) =>
    state.product.products.find((product) => product._id === productId)
  );
  
  const handleChange = (e) => {
    setInputs((prev) => {
      return { ...prev, [e.target.name]: e.target.value }
    })
  }
  
  const handleCat = (e) => {
    setCat(e.target.value.split(","));
  }

 
  useEffect(() => {
    if (cat.length === 0) {
      setCat(product.categories);
    }

  }, [])

  
  const handleClick = (e) => {
    e.preventDefault();
    const fileName = new Date().getTime() + file.name;
    const storage = getStorage(app);
    const StorageRef = ref(storage, fileName);
    const uploadTask = uploadBytesResumable(StorageRef, file);

     // Register three observers:
    // 1. 'state_changed' observer, called any time the state changes
    // 2. Error observer, called on failure
    // 3. Completion observer, called on successful completion
    uploadTask.on(
      "state_changed",
      (snapshot) => {
        // Observe state change events such as progress, pause, and resume
        // Get task progress, including the number of bytes uploaded and the total number of bytes to be uploaded
        const progress =
          (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
        console.log("Upload is " + progress + "% done");
        switch (snapshot.state) {
          case "paused":
            console.log("Upload is paused");
            break;
          case "running":
            console.log("Upload is running");
            break;
          default:
        }
      },
      (error) => {
        // Handle unsuccessful uploads
      },
      () => {
        // Handle successful uploads on complete
        // For instance, get the download URL: https://firebasestorage.googleapis.com/...
        getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
          const product = { ...inputs, img: downloadURL, categories: cat };
          // addProduct(dispatch, product);
          updateProduct(dispatch, product, productId);
        });
      }
    );
  }

    const MONTHS = useMemo(
        () => [
          "Jan",
          "Feb",
          "Mar",
          "Apr",
          "May",
          "Jun",
          "Jul",
          "Agu",
          "Sep",
          "Oct",
          "Nov",
          "Dec",
        ],
        []
    );

    useEffect(() => {
        const getStats = async () => {
          try {
            const res = await userMethod.get("orders/income?pid=" + productId);
            const list = res.data.sort((a,b)=>{
                return a._id - b._id
            })
            list.map((item) =>
              setProductStats((prev) => [
                ...prev,
                { name: MONTHS[item._id - 1], Sales: item.total },
              ])
            );
          } catch (err) {
            console.log(err);
          }
        };
        getStats();
      }, [productId, MONTHS])

  return (
    <div className="product">
      <div className="productTitleContainer">
        <h1 className="productTitle">Product</h1>
        <Link to="/newproduct">
          <button className="productAddButton">Create</button>
        </Link>
      </div>
      <div className="productTop">
          <div className="productTopLeft">
              <Chart data={productStats} dataKey="Sales" title="Sales Performance"/>
          </div>
          <div className="productTopRight">
              <div className="productInfoTop">
                <img src={product.img} alt="img" className="productInfoImg" />
                <span className="productName">{product.title}</span>
              </div>
              <div className="productInfoBottom">
                  <div className="productInfoItem">
                      <span className="productInfoKey">id:</span>
                      <span className="productInfoValue">{product._id}</span>
                  </div>
                  <div className="productInfoItem">
                      <span className="productInfoKey">sales:</span>
                      <span className="productInfoValue">5123</span>
                  </div>
                  <div className="productInfoItem">
                      <span className="productInfoKey">in stock:</span>
                      <span className="productInfoValue">{product.inStock}</span>
                  </div>
              </div>
          </div>
      </div>
      <div className="productBottom">
          <form className="productForm">
              <div className="productFormLeft">
                  <label>Product Name</label>
                  <input name="title" type="text" placeholder={product.title} onChange={handleChange} />
                  <label>Product Descriiption</label>
                  <input name="desc" type="text" placeholder={product.desc} onChange={handleChange} />
                  <label>Price</label>
                  <input name="price" type="text" placeholder={product.price} onChange={handleChange} />
                  <label>Categories</label>
                  <input type="text" placeholder={product.categories} onChange={handleCat} />
                  <label>Materials</label>
                  <select name="materials" onChange={handleChange}>
                    <option selected disabled>{product.materials}</option>
                    <option value="">None</option>
                    <option value="Rubber">Rubber</option>
                    <option value="Polyurethane">Polyurethane</option>
                    <option value="Metal">Metal</option>
                    <option value="Plastic">Plastic</option>
                  </select>
                  <label>In Stock</label>
                  <select name="inStock" id="idStock" onChange={handleChange}>
                    <option value="true">Yes</option>
                    <option value="false">No</option>
                  </select>
              </div>
              <div className="productFormRight">
                  <div className="productUpload">
                      <img src={product.img} alt="" className="productUploadImg" />
                      <label for="file">
                        <Publish/>
                      </label>
                      <input type="file" id="file" style={{display:"none"}} 
                        onChange={(e) => setFile(e.target.files[0])} />
                  </div>
                  <button onClick={handleClick} className="productButton">Update</button>
              </div>
          </form>
      </div>
    </div>
  );
}

Solution

  • The product object is loaded asynchronously, which means it may take some time to retrieve data from the database and update the Redux store. While this process is ongoing, your component may render and attempt to access the img property. This is what causes the error to occur.

    To fix this error, you need to make sure that the product object is defined before trying to access its properties:

    {product && <img src={product.img} alt="img" className="productInfoImg" />}