0

I am trying to call the handleClick method whenever the user clicks on the button, but nothing on the page will render and I get the error "Uncaught ReferenceError: handleClick is not defined".

Implementation of the component:

import {createElement} from 'react';
import {add} from '../action/cart';
import {connect} from 'react-redux';
import styles from './styles.css';

handleClick = (id) => {
  add(id);
  this.setState((prevState) => ({
    ...prevState,
    items: prevState.items.map(
      (item) =>
        id === item.id
          ? {id, quantity: item.quantity + 1}
          : {...item}
    ),
  }));
};

const Product = ({add, id, title, image}) => (
  <div className={styles.product} onClick={handleClick(id)}>
    <img src={image} alt={title} className={styles.productImage}/>
    {title}
  </div>
);

export default connect(() => ({}), {add})(Product);

This shares state with the cart component:

const Cart = connect(
  () => ({}),
  {clear}
)(({items, clear, total}) => {
  return (
    <div>
      <Heading><FontAwesomeIcon icon={faShoppingCart} /> Cart</Heading>
      {items.length ? <button onClick={clear}>Clear all items</button> : null}
      <table>
        <thead>
          <tr>
            <th>Product</th>
            <th>Price</th>
            <th>Quantity</th>
            <th>Total</th>
          </tr>
        </thead>
        <tbody>
          {items.map(({...item}, id) => (
            <Item {...item} key={id} />
          ))}
        </tbody>
      </table>
      {items.length ?
        <div className={styles.total}>${total}</div>
        : <div>Your cart is empty!</div>}
    </div>);
});

export default connect((state) => {
  return {
    items: state.cart.items,
    total: reduce(
      (sum, {id, quantity}) => sum + products[id].price * quantity,
      0,
      state.cart.items
    ).toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,'),
  };
})(Cart);

It references this action:

import {ADD_ITEM, SET_QUANTITY, CLEAR_ITEMS} from './types';
import {createAction} from 'redux-actions';

export const add = createAction(ADD_ITEM);
export const setQuantity = createAction(SET_QUANTITY);
export const clear = createAction(CLEAR_ITEMS);

Which uses this reducer:

[ADD_ITEM]: (state, {payload: id}) => ({
    ...state,
    items: [
      ...state.items,
      {id, quantity: 1},
    ],
  }),
  • The keyword `this` is misused here: `this.handleClick()`; it does not represent what you think it does. – Randy Casburn Oct 17 '18 at 03:26
  • Read this: https://stackoverflow.com/questions/3127429/how-does-the-this-keyword-work – Randy Casburn Oct 17 '18 at 03:27
  • Ahh okay. I tried writing it with and without `this` and I am still getting the same error –  Oct 17 '18 at 03:31
  • right...now we can address the Error. You have not declared handleClick, you've only assigned it a value. Place `var` in front of the handleClick() assignment. – Randy Casburn Oct 17 '18 at 03:34
  • I had written an answer, but I think I missed something. What is supposed to be holding the state with `items`? Is there a `Cart` component somewhere? – Eric Palakovich Carr Oct 17 '18 at 03:49
  • @EricPalakovichCarr I've been looking at and trying to piece your answer in my code editor since! I've updated it to have my cart component that I am trying to share the state with. Currently, I am getting `product.js:36 Uncaught TypeError: Cannot read property 'items' of undefined` which makes sense because I haven't passed down items object and I am trying to figure out how to pass it down. Thanks so much for your help! –  Oct 17 '18 at 03:59
  • @EricPalakovichCarr ^ I meant to say Cannot read property of 'map'. It is able to read the previous state, but I'm not sure how to be sharing items –  Oct 17 '18 at 04:12
  • Hey @AdamGinther, sorry about going dark there. I updated my answer below. Let me know if it helps you get across the finish line :) – Eric Palakovich Carr Oct 17 '18 at 16:43

4 Answers4

1

You're creating a stateless component for Product, and this isn't something you should use inside of a stateless component (let alone setState from its handler). Instead, you should make Product a regular component, like so:

EDIT (removed previous code)

Ok, I see you've updated the code in your post. So, here's a few things that may be tripping you up:

If you haven't done so already, get rid of setState in handleClick. That logic should be inside of a redux action, since all your state appears to be in a redux state tree.

You're calling connect twice for Cart. Remove the first call, where:

const Cart = connect(
  () => ({}),
  {clear}
)(({items, clear, total}) => {

Should become:

const Cart = ({items, clear, total}) => {

And I think you meant this...

<tbody>
  {items.map(({...item}, id) => (
    <Item {...item} key={id} />
  ))}
</tbody>

...to be this (I'm assuming products exists somewhere in your codebase since you used it in the connect call for Cart):

<tbody>
  {items.map(({...item}, id) => (
    <Product {...products[id]} {...item} key={id} />
  ))}
</tbody>

And I think you meant this:

{
  total: reduce(
    (sum, {id, quantity}) => sum + products[id].price * quantity,
    0,
    state.cart.items
  ).toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,'),
}

to be this:

{
  total: state.cart.items.reduce(
    (sum, {id, quantity}) => sum + products[id].price * quantity,
    0,
  ).toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,'),
}

And then you need to add the clear prop from your removed connect call back into the remaining one, leaving you with:

export default connect(
  state => ({
    items: state.cart.items,
    total: state.cart.items.reduce(
      (sum, {id, quantity}) => sum + products[id].price * quantity,
      0,
    ).toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,'),
  }),
  {clear},
)(Cart);

And to get back to that setState you removed earlier, your reducer for ADD_ITEM should probably resemble something like this:

[ADD_ITEM]: (state, {payload: id}) => {
  const itemAlreadyInCart = state.items.find(i => i.id === id);
  if (itemAlreadyInCart) {
    return {
      ...state,
      items: state.items.map(
        (item) =>
          id === item.id
            ? {id, quantity: item.quantity + 1}
            : {...item}
      ),
    }
  }
  else {
    return {
      ...state,
      items: [
        ...state.items,
        {id, quantity: 1, },
      ],
    }
  }
},

I think all of that above should get you pretty close to everything working.

EDIT 2

To answer your comment, is it because you're not handling the CLEAR_ITEMS action in your reducer? Perhaps you need the reducer to look something like this?

[ADD_ITEM]: (state, {payload: id}) => {
  const itemAlreadyInCart = state.items.find(i => i.id === id);
  if (itemAlreadyInCart) {
    return {
      ...state,
      items: state.items.map(
        (item) =>
          id === item.id
            ? {id, quantity: item.quantity + 1}
            : {...item}
      ),
    }
  }
  else {
    return {
      ...state,
      items: [
        ...state.items,
        {id, quantity: 1, },
      ],
    }
  }
},

[CLEAR_ITEMS]: (state) => {
  return {
    ...state,
    items: [],
  }
},

By the way, I also noticed another problem. I had posted this change earlier:

<tbody>
  {items.map(({...item}, id) => (
    <Product {...products[id]} {...item} key={id} />
  ))}
</tbody>

But the id in map(({...item}, id) isn't the id key from the item, but the index of the array provided by the map function. You probably want to do something like this:

<tbody>
  {items.map(({id, ...item}) => (
    <Product {...products[id], ...item, id} key={id} />
  ))}
</tbody>
Eric Palakovich Carr
  • 22,701
  • 8
  • 49
  • 54
  • This is awesome! I appreciate your help so much!! Thanks for explaining all of this. One thing I am unclear on is how dispatch works now. With the new way of dispatching it ` –  Oct 18 '18 at 05:18
  • @AdamGinther See EDIT 2 in my updated answer. Let me know if that did the trick. – Eric Palakovich Carr Oct 18 '18 at 13:14
  • Thanks again! I hadn't realized that the functionality for clearing wasn't in the code that I pasted, but clear items exists in the reducer as: `[CLEAR_ITEMS]: () => ({ items: [], }),` Also, after your first edit I found this to work for mapping: `{items.map(({...item}, id) => ( ))}` –  Oct 18 '18 at 19:52
  • I am thinking that the dispatch is not being called because of the clear prop being removed and added into the connect callback, but I'm not sure why it wouldn't be called –  Oct 18 '18 at 20:05
  • Ugh, I realized I'm dumb. The second argument in connect (i.e. mapDispatchToProps) when passed in as an object of action creators will wrap each one as dispatch. I updated the code above...and my bad :P Have you considered installing something like [redux-logger](https://www.npmjs.com/package/redux-logger) to get a better look at which actions are getting dispatched? – Eric Palakovich Carr Oct 19 '18 at 00:31
0

Seems method is not getting bind to event. You can try explicitly by using .bind method

MukundK
  • 86
  • 2
0

it's a little hard to tell from your example, but if you are passing a method from a parent component into a child component, you need to bind it. This can be done several ways, but a straightforward place to start would be onClick={handleClick(id).bind(this)}

However, it isn't clear how you are 'sharing state' in this example. If you can clarify the structure of your app I'll clarify how to appropriately bind the method.

Sarah Hailey
  • 494
  • 5
  • 19
  • ITS an arrow function and no need of manual binding. Also FYI- binding should happen only in constructor – Hemadri Dasari Oct 17 '18 at 04:22
  • Think-Twice can you give more info about why it should only happen in constructor? I've seen it work successfully happening as described. I would also love to see an example of an arrow function auto-binding a parent method to a child component, if you have an example on hand. – Sarah Hailey Oct 17 '18 at 04:30
  • Thanks @SarahHailey - At the structure of my App, I have the Cart component and the Products component. The products component renders three product components with data. The component that is rendered three times is seen at the top of my question. Each of these items can be clicked on, and it will add an item. The cart component maps through these items and displays them with options to alter them such as incrementing, decrementing, and deleting them. All of these components exist within the same "component" folder –  Oct 17 '18 at 04:40
  • @AdamGinther unfortunately there are so many syntax issues with your example that I can't recreate it. Can share a repository or gist that has the full code? – Sarah Hailey Oct 17 '18 at 04:48
  • @SarahHailey the reasn you need to bind the function only constructor because webpack will create a new function only one in bundle file because constructor loads only once per component but if do binding directly in render or in event handler functions whenever the component renders it will create a new function in bundle file so the bundle file size increases hence we are recommended to do binding only in constructor always. when we use arrow function the this context is available and you no need to bind the arrow function. Please et me know if you have further queries – Hemadri Dasari Oct 17 '18 at 08:33
0

Since handleClick does setState I would recommend you to move the event handler function to class component also known as statefull component and pass down the function as prop to Product component. Because Product component is a functional or stateless component it’s not you not recommended to mutate the state. If you want to handle handleClick with in Product component itself then change Product component to statefull component

Also handleClick is an arrow function so you no need to do manual binding.

Hemadri Dasari
  • 32,666
  • 37
  • 119
  • 162