user1896446
user1896446

Reputation:

Method returning as undefined when using connect from React-redux

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},
    ],
  }),

Upvotes: 0

Views: 166

Answers (4)

Eric Palakovich Carr
Eric Palakovich Carr

Reputation: 23338

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>

Upvotes: 1

Hemadri Dasari
Hemadri Dasari

Reputation: 34014

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.

Upvotes: 0

Sarah Hailey
Sarah Hailey

Reputation: 494

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.

Upvotes: 0

MukundK
MukundK

Reputation: 86

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

Upvotes: 0

Related Questions