I am trying to introduce 'cartItems' functionality to my react-redux app and store the added data in the browser's local storage.
Indeed the problem raises when I try to show cart items by clicking on the cart link at the navbar section. The error message is 'GET http://localhost:3000/products/undefined 500 (Internal Server Error)' and 'Uncaught (in promise)'. and I don't know how to fix the issue.
Note: the same component 'CartScreen.js' would display the cart items in both cases, when adding new items to the cart & when also clicking on the cart link at the navbar.
Please follow the code snippets
Thanks & Regards
App.js
import Header from './components/Header';
import { Container } from 'react-bootstrap';
import HomeScreen from './screens/HomeScreen';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import ProductScreen from './screens/ProductScreen';
import CartScreen from './screens/CartScreen';
function App() {
return (
<Router>
<Header />
<main className="py-3">
<Container>
<Routes>
<Route path="/" element={<HomeScreen />} exact />
<Route path="/product/">
<Route path=":id" element={<ProductScreen />} />
<Route index element={<ProductScreen />} />
</Route>
<Route path="/cart" >
<Route index element={<CartScreen />} />
<Route path=":productid" element={<CartScreen />} />
</Route>
</Routes>
</Container>
</main>
<Footer />
</Router>
);
}
export default App;
ProductScreen.js
import { useParams, Link, useNavigate } from 'react-router-dom';
import {Row,Col,Image,ListGroup,Button,Card,Form} from 'react-bootstrap';
import Rating from '../components/Rating';
import { listProductDetails } from '../actions/productActions';
import { useDispatch, useSelector } from 'react-redux';
import Loader from '../components/Loader';
import Message from '../components/Message';
function ProductScreen() {
const { id } = useParams();
const navigate = useNavigate();
const [qty, setQty] = useState(1);
const dispatch = useDispatch();
const productListDetail = useSelector((state) => state.productDetail);
const { loading, error, product } = productListDetail;
useEffect(() => {
dispatch(listProductDetails(id));
}, [dispatch, id]);
const addToCartHandler = () => {
navigate(`/cart/${id}?qty=${qty}`);
};
return (
<div> <Link to={-1} className="btn btn-primary my-3">Go Back</Link>
{loading ? (<Loader />): error ? (<Message variant="danger">{error}</Message>) : (
<Row>
<Col md={6}>
<Image src={product.image} alt={product.name} fluid />
</Col>
<Col md={3}>
<ListGroup variant="flush">
<ListGroup.Item>
<h3> {product.name}</h3>
</ListGroup.Item>
<ListGroup.Item>
<Rating value={product.rating} text={`${product.numReviews} reviews`}
color={'#fae500'}/>
</ListGroup.Item>
<ListGroup.Item>Price: ${product.price}</ListGroup.Item>
<ListGroup.Item>
Description: {product.description}
</ListGroup.Item>
</ListGroup>
</Col>
<Col md={3}>
<Card>
<ListGroup variant="flush">
<ListGroup.Item>
<Row>
<Col> Price: </Col>
<Col>
<strong>${product.price} </strong>
</Col>
</Row>
</ListGroup.Item>
<ListGroup.Item>
<Row>
<Col> Status: </Col>
<Col>
<strong>
{product.countInStock > 0 ? 'In Stock' : 'Out of Stock'}
</strong>
</Col>
</Row>
</ListGroup.Item>
{product.countInStock > 0 && (
<ListGroup.Item>
<Row>
<Col> Qty </Col>
<Col xs="auto" className="my-1">
<Form.Control as="select" value={qty}
onChange={(e) => setQty(e.target.value)}>
{[...Array(product.countInStock).keys()].map((x) => (
<option key={x + 1} value={x + 1}>{x + 1}</option>))}
</Form.Control>
</Col>
</Row>
</ListGroup.Item>)}
<ListGroup.Item>
<Button onClick={addToCartHandler}
className="btn btn-primary container-fluid"
disabled={product.countInStock === 0}
type="button">
Add to Cart
</Button>
</ListGroup.Item>
</ListGroup>
</Card>
</Col>
</Row>
)}
</div>
);
}
export default ProductScreen;
CartScreen.js
import React, { useEffect } from 'react';
import { Col, ListGroup,Row,} from 'react-bootstrap';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation, useNavigate, useParams, Link, Outlet } from 'react-router-dom';
import { addToCart } from '../actions/cartAction';
import Message from '../components/Message';
const CartScreen = () => {
const { search } = useLocation();
const { productid } = useParams();
const qty = search ? Number(search.split('=')[1]) : 1;
const dispatch = useDispatch();
const cart = useSelector((state) => state.cart);
const { cartItems } = cart;
useEffect(() => {
dispatch(addToCart(productid, qty));
},[dispatch, productid, qty]);
return (
<Row>
<Col md={8}> {cartItems.length === 0 ? (<Message variant="info">
Go Back To Home Page <Link to="/"></Link> </Message> ) : (
<ListGroup> {cartItems.map((x) => (
<ListGroup.Item key={x.product}>
{x.name} , {x.qty}
</ListGroup.Item> ))}
</ListGroup>)}
</Col>
<Col md={4}></Col>
</Row>
);
};
export default CartScreen;
cartReducers.js
import { CART_ADD_ITEM } from '../constants/cartConstants';
export const cartReducer = (state = { cartItems: [] }, action) => {
switch (action.type) {
case CART_ADD_ITEM:
const item = action.payload;
const existItem = state.cartItems.find((x) => x.product === item.product);
if (existItem) {
return {
...state, cartItems: state.cartItems.map((x) =>
x.product === existItem.product ? item : x),};}
else {
return {
...state, cartItems: [...state.cartItems, item],};}
default:
return state;
}
};
cartAction.js
import axios from 'axios';
import { CART_ADD_ITEM } from '../constants/cartConstants';
export const addToCart = (productid, qty) => async (dispatch, getState) => {
const { data } = await axios.get(`/products/${productid}`);
dispatch({
type: CART_ADD_ITEM,
payload: {
product: data._id,
name: data.name,
image: data.image,
price: data.price,
countInStock: data.countInStock,
qty,
},
});
localStorage.setItem('cartItems', JSON.stringify(getState().cart.cartItems));
};
store.js
import { legacy_createStore, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { composeWithDevTools } from '@redux-devtools/extension';
import {
productDetailsReducer,
productListReducer,
} from './reducers/productReducers';
import { cartReducer } from './reducers/cartReducers';
const reducer = combineReducers({
productList: productListReducer,
productDetail: productDetailsReducer,
cart: cartReducer,
});
const cartItemsFromStorage = localStorage.getItem('cartItems')
? JSON.parse(localStorage.getItem('cartItems'))
: [];
const initialState = { cart: { cartItems: cartItemsFromStorage } };
const middleware = [thunk];
const store = legacy_createStore(
reducer,
initialState,
composeWithDevTools(applyMiddleware(...middleware))
);
export default store;
It seems the issue here might be resolved by using the same logic in my answer here to your other question regarding route matching. It wasn't explicitly called out as an issue or something the needed to be addressed/fixed (in other words, I thought you'd used the code and had an issue elsewhere), so adding an answer here for the specific axios issue and resolution.
It looks like navigating to "/cart"
will result in both productid
and qty
being undefined/invalid values, and the useEffect
hook is unconditionally dispatching the action to add the item & quantity. productid
is undefined at axios.get(`/products/${productid}`)
in the action creator.
You should only dispatch the addToCart
action if there is a valid product id and a quantity to add.
const CartScreen = () => {
const { search } = useLocation();
const { productid } = useParams();
const qty = search ? Number(search.split('=')[1]) : 1;
const dispatch = useDispatch();
const cart = useSelector((state) => state.cart);
const { cartItems } = cart;
useEffect(() => {
if (productid && qty > 0) {
dispatch(addToCart(productid, qty)); // <-- only dispatch if valid
}
}, [dispatch, productid, qty]);
return (
<Row>
<Col md={8}>
{!cartItems.length
? (
<Message variant="info">
<Link to="/">
Go Back To Home Page
</Link>
</Message>
) : (
<ListGroup>
{cartItems.map((x) => (
<ListGroup.Item key={x.product}>
{x.name} , {x.qty}
</ListGroup.Item>
))}
</ListGroup>
)
}
</Col>
<Col md={4}></Col>
</Row>
);
};