1

I’m building an e-commerce website using React, Redux, and React-Router. So far, I have a home page with all items and a link to add the items to the cart. There are <Link>s to an item details page for each item, and this item details page also has an “add to cart” button.

I need the addToCart action to take in the ID of the item being viewed, and add it to the cart. My problem is that on the item details page, when the “add to cart” button is clicked, I receive this TypeError for the cartReducer.js file: TypeError: Cannot set property 'quantity' of undefined

This only happens from the item details page, but not from the home page. I have a feeling this has to do with the fact that all of the items are rendered from a map() function on the home page...?

On the item details page, the item is rendered based on props (location.state) which are passed from the <Link> from the home page. I can’t figure out how to pass the item ID from the URL to the addToCart action without it throwing errors.


This is what I’ve tried so far:

Item.js

const mapStateToProps = (state, ownProps) => {
    return {
        items: state.items,
        id: ownProps.id === state.id
        // I've also tried id: ownProps.id
    };
}

I realize that trying the above didn't work for me, so I've taken ownProps out of the mapStateToProps() in Item.js completely.

I’ve read many other SO questions & answers and informational articles on this topic today, but I’ve lost track of all that I have referenced.

I just don't understand why the item ID can't be passed to the reducer from Item.js, yet it works perfectly well for Home.js


I have put the code on codesandbox here:

https://codesandbox.io/s/github/thesemdrnsocks/redux-co

Here is my code so far:

Item.js (renders the item details page--adding an item to the cart from this component does not work)

import React from 'react';
import { connect } from 'react-redux';
import {
    Link,
    withRouter
} from 'react-router-dom';
import { addToCart } from '../../actions/CartActions';

const mapStateToProps = (state, ownProps) => {
    return {
        items: state.items,
        id: ownProps.id === state.id
    };
}

const mapDispatchToProps = (dispatch) => {
    return {
        addToCart: (id) => { dispatch(addToCart(id)) }
    };
}

class Item extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            item: this.props.location.state,
        }

    }

    handleAddToCart = (id) => {
        this.props.addToCart(id);
    }

    render() {
        const item = this.state.item;
        return (
            <div className="item-details" key={item.id}>
                <div className="item-details-img">
                    <img src={item.img} alt={item.title} />
                </div>
                <div className="item-details-info">
                    <p className="item-details-title">{item.title}</p>
                    <p className="item-details-price"><b>${item.price}</b></p>
                    <p className="item-details-desc">{item.desc}</p>
                    <div className="item-details-add-to-cart">
                        <Link to="/cart" onClick={ () => { this.handleAddToCart(item.id) } }>
                            <button>Add to Bag</button>
                        </Link>
                    </div>
                </div>
            </div>
        );
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(withRouter(Item));

Home.js (renders the home page, which displays all items in the store--adding an item to the cart works from here)

import React from 'react';
import { connect } from 'react-redux';
import {
    Link,
    withRouter
} from 'react-router-dom';
import { addToCart } from '../actions/CartActions';
import Item from './shopping/Item';

const mapStateToProps = (state) => {
    return {
        items: state.items
    };
}

const mapDispatchToProps = (dispatch) => {
    return {
        addToCart: (id) => { dispatch(addToCart(id)) }
    };
}

class Home extends React.Component {
    handleAddToCart = (id) => {
        this.props.addToCart(id);
    }

    render() {
        let itemList = this.props.items.map(item =>{
            return (
                <div className="home-item-card" key={item.id}>
                    <div className="home-item-image">
                        <Link to = 
                            {{ pathname: `/products/${item.category}`,
                            search: `?id=${item.id}`,
                            state: {
                                id: `${item.id}`,
                                img: `${item.img}`,
                                title: `${item.title}`, 
                                desc: `${item.desc}`,
                                price: `${item.price}`
                            } }}
                            component={ Item }>
                                <img src={item.img} alt={item.title} />
                        </Link>
                    </div>
                    <div className="home-item-info">
                        <span className="home-item-title">{item.title}</span>
                        <Link to="/" className="home-add-item" onClick={() => 
                            { this.handleAddToCart(item.id) } }>
                            <i class="fa fa-plus-circle"></i>
                        </Link>
                        <p className="home-item-price"><b>${item.price}</b></p>
                        <p className="home-item-desc">{item.desc}</p>
                    </div>
                </div>
            )
        })

        return (
            <div className="home-new-arrivals">
                <h1>What's new?</h1>
                <div className="new-arrivals-items">
                    {itemList}
                </div>
            </div>
        );
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(withRouter(Home));

cartReducer.js (an excerpt)

const initState = {
    items: ShopContent,
    addedItems: [],
    total: 0
}

const cartReducer = (state = initState, action) => {
    // If item is added to cart...
    if (action.type === ADD_TO_CART) {
        let addedItem = state.items.find(item => item.id === action.id);
        let existedItem = state.addedItems.find(item => action.id === item.id);

        // Check if item already exists in cart
        // If item is already in cart, increase quantity by 1, and calculate total
        if (existedItem) {
            addedItem.quantity += 1;
            return {
                ...state,
                total: state.total + addedItem.price
            }
        } else {
            // Add item to cart and calculate total
            addedItem.quantity = 1;
            let newTotal = state.total + addedItem.price;
            return {
                ...state,
                addedItems: [...state.addedItems, addedItem],
                total: newTotal
            }
        }
    }

/* ... the rest of the reducer covers changing quantity of items
in cart and adding/removing shipping from the cart total */

ShopContent.js (an excerpt; holds item information)

import Item1 from '../images/item1.png';
// ... followed by more imports for each item's image

export const ShopContent = [
    { 
        id: 1, 
        title: 'Black Eyelet Dress', 
        desc: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
        price: 120,
        img: Item1,
        category: 'dresses',
        sale: false
    },
// ... followed by more items...
Jenna
  • 489
  • 6
  • 13
  • you have a bug in your code. `addedItem ` in the reducer can be undefined if your `find` search doesn't return a value. This would happen for something that doesn't exist in your array of items – John Ruddell Sep 08 '19 at 22:23
  • I will keep this in mind, make some changes, and report back later. Thanks for the insight! – Jenna Sep 08 '19 at 22:54
  • Your comment about the `find` function makes sense. What doesn’t make sense to me is that the item ID is being passed with `` from the `Home.js` _to_ `Item.js`, but it is still `undefined` in the reducer when being used from `Item.js`… It seems like the reducer can access the ID from `Home.js`, but `Item.js` can’t pass the right information to the reducer, even though it seems `Item.js` has access to the same information as `Home.js` component. I’ve edited my question to include `Home.js`. Thanks again for the insight. – Jenna Sep 09 '19 at 18:12
  • Would you be able to replicate the issue on a codesandbox? its hard to debug an issue on guesses :) one thing that does look off is `id: ownProps.id === state.id` That is a boolean rather than an ID. Also, why are you doing an `addToCart` in home and item? shouldn't home just render item and let item do the add to cart? – John Ruddell Sep 09 '19 at 18:25
  • I have changed that `ownProps.id === state.id`, but it didn't seem to help. And I have changed the `Home.js` configuration, it was looking a little cluttered with the add to cart and description, when the user could see that information on the `Item.js` page when they clicked through. Here is a link to the codesandbox: https://codesandbox.io/s/github/thesemdrnsocks/redux-co – Jenna Sep 09 '19 at 18:55

2 Answers2

1

Your issue here is you mutate the data type of your item.id when passing to the Item component. You're putting the selected item in location history as a string. but then do a === in the reducer on an integer to string, which wont work. (aka 1 === '1').

<Link to={
  { pathname: `/products/${item.category}`,
    search: `?id=${item.id}`,
    state: {
      id: `${item.id}`,
      img: `${item.img}`,
      title: `${item.title}`, 
      desc: `${item.desc}`,
      price: `${item.price}`
    }
  }}
  component={ Item }
>
  <img src={item.img} alt={item.title} />
</Link>

A few ways you can fix this.
1. Pull the item from your actual state based on the id in the URL.
2. Dispatch an active item action that updates your reducer with an active item that you're viewing.

I think the first option is pretty straight forward and introduces less changes to your code, this is what that would look like.

// Home
<Link
  to={{
    pathname: `/products/${item.category}`,
    search: `?id=${item.id}`
  }}
  component={Item}
>
  <img src={item.img} alt={item.title} />
</Link> 

And then in item,

// Item
constructor(props) {
  super(props);
  let currentItemId = new URLSearchParams(window.location.search).get("id");
  if (currentItemId) {
    currentItemId = parseInt(currentItemId, 10);
  }
  this.state = {
    item: props.items.find(item => item.id === currentItemId)
  };
}
John Ruddell
  • 22,365
  • 5
  • 46
  • 79
  • 1
    Thank you for this—your solution and thorough explanation is extremely helpful. It seems so obvious now, I feel like I have some brushing up on basic JS to do… I’ll implement this strategy into my current code, and be sure to consult the MDN docs along the way. Thank you again for your time and patience with this! – Jenna Sep 09 '19 at 19:50
  • @Jenna happy to help! if you want more help on how I figured it out I can give debugging tips and stuff :) – John Ruddell Sep 09 '19 at 20:03
  • Sure, that would be helpful! I could improve my debugging skills. – Jenna Sep 09 '19 at 20:16
  • 1
    @Jenna so, first is to find where the issue is `TypeError: Cannot set property 'quantity' of undefined`. This means you're essentially calling `undefined.quantity = something`. Once finding where you set the quantity, i put a `debugger` line in the reducer right after you created `addedItem` and opened the dev tools to see if its undefined. After confirming its undefined, I looked at the condition you are using to find the `item`. then looked at `action.id` to see what it was, which is where I saw `'1'`. all of this is when the debugger breakpoint fired so you can inspect the actual values. – John Ruddell Sep 09 '19 at 20:38
  • 1
    after figuring out that its a string id, I looked at the action function, but nope thats a straight pass through, so I went to the component `Item.js` to see how its called. Since there was no mutation of it there but seeing (`item: this.props.location.state`) in the constructor, I looked for where it was being set in the location state. Found it in `Home.js`. :) Remember to always start with where the issue is, debug it to see what is acutally not working. Then logically follow that up the tree to figure out where the mistake is! Hope that helps! :) – John Ruddell Sep 09 '19 at 20:42
  • 1
    Those tips do help. I haven't been using DevTools much (just logged some things to the console and a little bit of mobile development, but nothing too crazy). I will explore it some more--especially for debugging! – Jenna Sep 09 '19 at 21:11
0

I do have a feeling this is where your problem is, you're passing a boolean value to the id props. It should be either ownProps.id or state.id

const mapStateToProps = (state, ownProps) => {
    return {
        items: state.items,
        id: ownProps.id === state.id
    };
}
Claeusdev
  • 399
  • 2
  • 11
  • 1
    I’ve changed `mapStateToProps()` to _not_ contain a boolean, but I am still getting the same error… Thank you for the reply! – Jenna Sep 09 '19 at 18:12