Reputation: 9
I have been writing an application (e-Commerce, as a project, following a tutorial) with React. I am getting an error of 'TypeError: Cannot read property 'length' of undefined' when referring to a cart object. Here's some background context. I am generating the cart object using a useState hook near the top of my App.js component:
const [cart, setCart] = useState({});
A little further down in App.js a console.log statement executes without errors suggesting that the cart object exists:
console.log(cart);
However, when I try to pass the cart object to a Cart component in the render section of App.js the aforementioned error (e.g. 'TypeError: Cannot read property 'length' of undefined') is generated. Why is this happening and how do I fix it?
Here is the code of App.js
import React, { useState, useEffect } from 'react'
import { commerce } from './lib/commerce';
import { Products, Navbar, Cart } from './components';
const App = () => {
const [products, setProducts] = useState([]);
const [cart, setCart] = useState({});
const fetchProducts = async () => {
const { data } = await commerce.products.list();
setProducts(data);
}
const fetchCart = async () => {
setCart(await commerce.cart.retrieve());
}
const handleAddToCart = async (productID, quantity) => {
const item = await commerce.cart.add(productID, quantity);
setCart(item.cart);
}
useEffect(() => {
fetchProducts();
fetchCart();
}, []);
console.log(cart);
return (
<div>
<Navbar totalItems={cart.total_items} />
{/* <Products products={products} onAddToCart={handleAddToCart} /> */}
<Cart cart={cart} />
</div>
)
}
export default App
And here is the code of the component (Cart) that I am passing the cart object into:
import React from 'react'
import { Container, Typography, Button, Grid } from "@material-ui/core";
import useStyles from './styles';
const Cart = ({ cart }) => {
const isEmpty = !cart.line_items.length;
const classes = useStyles();
const EmptyCart = () => {
<Typography variant="subtitle1">
You have no items your shopping cart..
</Typography>
}
const FilledCart = () => {
<>
<Grid container spacing={3}>
{
cart.line_items.map((item) => (
<Grid item xs={12} sm={4} key={item.id}>
<div>{item.name}</div>
</Grid>
))
}
</Grid>
<div className={classes.cardDetails}>
<Typography variant="h4">
Subtotal: { cart.subtotal.formatted_with_symbol }
</Typography>
<div>
<Button className={classes.emptyButton} size="large" variant="contained" type="button" color="secondary">Empty Cart</Button>
<Button className={classes.checkoutButton} size="large" variant="contained" type="button" color="primary">Checkout</Button>
</div>
</div>
</>
}
return (
<Container>
<div className={classes.toolbar} />
<Typography className={classes.title} variant="h3">Your shopping cart</Typography>
{
isEmpty ? <EmptyCart /> : <FilledCart />
}
</Container>
)
}
export default Cart
Upvotes: 0
Views: 754
Reputation: 81
There is another way to approach the error (it solved my problem).
We only need to add this line:
if (!cart.line_items) return "Loading...";
and then remove the variable from the top and add it inside of the if statement:
{!cart.line_items.length ? <EmptyCart /> : <FilledCart />}
as sometimes we don't need to create a variable if the content is meaningful enough on its own.
The code:
import React, { useEffect } from "react";
import { Container, Typography, Button, Grid } from "@material-ui/core";
import useStyles from "./styles";
//
//
const Cart = ({ cart }) => {
// const isEmpty = !(cart.line_items && cart.line_items.length);
const classes = useStyles();
//
//So if the cart is EMPTY show the following:
const EmptyCart = () => {
<Typography variant="subtitle1">
You have no items in your shopping cart, start adding some!
</Typography>;
};
//
//
//So if the cart is FILLED show the following:
const FilledCart = () => (
<>
<Grid container spacing={3}>
{cart.line_items.map((item) => (
<Grid item xs={12} sm={4} key={item.id}>
<div>{item.name}</div>
</Grid>
))}
</Grid>
<div className={classes.cardDetails}>
<Typography variant="h4">
Subtotal: {cart.subtotal?.formatted_with_symbol}
</Typography>
<div>
<Button
className={classes.emptyButton}
size="large"
type="button"
variant="contained"
color="secondary"
>
Empty cart
</Button>
<Button
className={classes.checkoutButton}
size="large"
type="button"
variant="contained"
color="primary"
>
Checkout
</Button>
</div>
</div>
</>
);
//
if (!cart.line_items) return "Loading";
return (
<Container>
<div className={classes.toolbar} />
<Typography className={classes.title} variant="h3">
Your shopping Cart
</Typography>
{!cart.line_items.length ? <EmptyCart /> : <FilledCart />}
</Container>
);
};
export default Cart;
Upvotes: 0
Reputation: 202605
The issue with your code is that the initial state doesn't match what you access on the initial render.
In App
component the cart
state is just an empty object.
const [cart, setCart] = useState({});
cart
is passed as a prop to Cart
component and the code assumes cart.line_items
is defined in order to access a length
property or map
function. cart.line_items
is OFC undefined so attempting to access the length
property (or map
) throws the TypeError: Cannot read property 'XYZ' of undefined
const isEmpty = !cart.line_items.length;
and
cart.line_items.map(.....
but when I console.log it out in App.js, it actually does print out the necessary information.
The console.log(cart);
is in the function body of the component so it's incorrectly logging the cart
state as an unintentional side-effect, it should be logged from a useEffect
hook so you see the value per render cycle. The other issue here is that you aren't accessing any nested properties so this will never throw an error. I'd be willing to bet that with this code you have at least 1 or 2 logs entires that are just empty objects ({}
) and then you see some logs with populated nested properties.
Example possible log output:
{} {} { line_items: [.....], subtotal: ..... } { line_items: [.....], subtotal: ..... }
Regarding the state logging, you should use an useEffect
hook with a dependency on the state value you are logging. This will log the cart
state on the initial render and later only when the cart
state value is updated.
useEffect(() => {
console.log(cart);
}, [cart]);
For the error, you've several options to help guard against errors when accessing your cart
state.
Provide valid initial state that matches what is accessed during the render cycle, add line_items: []
to the initial cart
state such that cart.line_items
will now exist and have a length
property.
const [cart, setCart] = useState({ line_items: [] });
Use a guard clause or Optional chaining on the passed cart
prop.
const isEmpty = !(cart.line_items && cart.line_items.length);
or
const isEmpty = !cart.line_items?.length);
and
cart.line_items && cart.line_items.map(.....
or
cart.line_items?.map(.....
May as well guard the subtotal sub-state as well in the case that cart.subtotal
isn't defined.
<Typography variant="h4">
Subtotal: { cart.subtotal?.formatted_with_symbol }
</Typography>
Upvotes: 3