PandasHelp
PandasHelp

Reputation: 15

ReactJS: How does bind work in this case?

I'm currently building an eCommerce website with an Add To Cart feature, I'm working within the Cart class. I'm using Redux -- the onDelete function is used for when there's a Delete button and I can remove a book from the current cart state by clicking it (calling onClick).

This is what I've been trying but it does not work and throws the error: "TypeError: Cannot read property 'onClick' of undefined".

You can see that I've passed this to the onClick function in the constructor so in the onClick property of the Button, I have simply passed the cartID into the onClick function:

Cart.js file

class Cart extends React.Component {
    constructor() {
        super();
        this.onClick = this.onDelete.bind(this);
    }

    onDelete(_id) {
    //
    }

    render() {
        // if more than 1 item in cart array, then render the cart
        // otherwise, render empty
        if(this.props.cart[0]) {
            return this.renderCart();
        } else {
            return this.renderEmpty();
        }       
    }

    renderEmpty() {
        return (<div>Empty Cart</div>)
    }

    renderCart() {
        
        const cartItemsList = this.props.cart.map(function(cartArr) {
            return (
                <Card key={cartArr._id}>
                    <Row>
                        <Col>
                            <h6>{cartArr.title}</h6>
                        </Col>
                        <Col>
                            <h6>USD {cartArr.price}</h6>
                        </Col>
                        <Col>
                            <h6>qty. goes here</h6>
                        </Col>
                        <Col>
                            <ButtonGroup style={{minWidth:'300px'}}>
                                <Button
                                    variant = "default"
                                    size    = "small">
                                    -
                                </Button>
                                <Button
                                    variant = "default"
                                    size    = "small">
                                    +
                                </Button>
                                <span>     </span>
                                <Button
                                    onClick = {this.onClick(cartArr._id)}
                                    variant = "danger"
                                    size    = "small">
                                    DELETE
                                </Button>
                            </ButtonGroup>
                        </Col>
                    </Row>
                </Card>
            )
        })

        return (
            <Card>
                {cartItemsList}
            </Card>
        )
    }
}

I know that a part of this is due to the nature of the this keyword that I am passing to the onClick function. It works fine when I follow a tutorial and add the this keyword at the end of the cartItemList:

class Cart extends React.Component {
    constructor() {
        super();
        this.onClick = this.onDelete.bind(this);
    }

    onDelete(_id) {
    //
    }

    render() {
        // if more than 1 item in cart array, then render the cart
        // otherwise, render empty
        if(this.props.cart[0]) {
            return this.renderCart();
        } else {
            return this.renderEmpty();
        }       
    }

    renderEmpty() {
        return (<div>Empty Cart</div>)
    }

    renderCart() {
        
        const cartItemsList = this.props.cart.map(function(cartArr) {
            return (
                <Card key={cartArr._id}>
                    <Row>
                        <Col>
                            <h6>{cartArr.title}</h6>
                        </Col>
                        <Col>
                            <h6>USD {cartArr.price}</h6>
                        </Col>
                        <Col>
                            <h6>qty. goes here</h6>
                        </Col>
                        <Col>
                            <ButtonGroup style={{minWidth:'300px'}}>
                                <Button
                                    variant = "default"
                                    size    = "small">
                                    -
                                </Button>
                                <Button
                                    variant = "default"
                                    size    = "small">
                                    +
                                </Button>
                                <span>     </span>
                                <Button
                                    onClick = {this.onClick(cartArr._id)}
                                    variant = "danger"
                                    size    = "small">
                                    DELETE
                                </Button>
                            </ButtonGroup>
                        </Col>
                    </Row>
                </Card>
            )
        })

        return (
            <Card>
                {cartItemsList}
            </Card>
        )
    }
}

Upvotes: 0

Views: 66

Answers (4)

Bartek Fryzowicz
Bartek Fryzowicz

Reputation: 6674

In addition to other answers I'd like to add explanation why your code throws "TypeError: Cannot read property 'onClick' of undefined". error.

You define map callback as regular function (not an arrow function) and according to map docs:

If a thisArg parameter is provided, it will be used as callback's this value. Otherwise, the value undefined will be used as its this value.

So this inside your map callback is undefined, that's why you get an error. Although onClick function is correctly 'binded' you can't access this function because this inside map callback doesn't point to component instance. To fix it you can use an arrow function as map callback instead of regular function (because inside arrow function this value of the enclosing lexical scope is used).

Upvotes: 1

ASDF
ASDF

Reputation: 351

You should use currying.

When you write this.onClick(cartArr._id) it evaluates to undefined but it expects to get a callback function. So you need to return a function from onDelete, like this:

onDelete(id) {
    return function() {
         //On delete logic
    }
}

By the way, you can avoid binding the callback by using class properties and arrow functions(https://medium.com/quick-code/react-quick-tip-use-class-properties-and-arrow-functions-to-avoid-binding-this-to-methods-29628aca2e25)

Hope that helps, good luck!

Upvotes: 1

AKX
AKX

Reputation: 168863

With your syntax

onClick = {this.onClick(cartArr._id)}

the function gets called immediately, not on-click.

You'll need

onClick = {() => this.onClick(cartArr._id)}

instead.

Anyway, with modern ES there's no need to manually bind things; use arrow functions instead.

Here's how I'd write your component (if it had to be a class component, not a function component):

class Cart extends React.Component {
  // Arrow functions are automagically bound
  onDelete = (_id) => {
    const { cart } = this.props;
    const cartWithoutGivenId = cart.filter((item) => item._id !== _id);
    const cartAfterDelete = { books: cartWithoutGivenId };
    this.props.deleteFromCart(cartAfterDelete);
  };

  render() {
    const { cart } = this.props;
    if (!cart.length) {
      return <div>Empty Cart</div>;
    }

    const cartItemsList = cart.map((item) => (
      <Card key={item._id}>
        <Row>
          <Col>
            <h6>{item.title}</h6>
          </Col>
          <Col>
            <h6>USD {item.price}</h6>
          </Col>
          <Col>
            <h6>qty. goes here</h6>
          </Col>
          <Col>
            <ButtonGroup style={{ minWidth: "300px" }}>
              <Button variant="default" size="small">
                -
              </Button>
              <Button variant="default" size="small">
                +
              </Button>
              <Button
                onClick={() => this.onDelete(item._id)}
                variant="danger"
                size="small"
              >
                DELETE
              </Button>
            </ButtonGroup>
          </Col>
        </Row>
      </Card>
    ));

    return <Card>{cartItemsList}</Card>;
  }
}

edit

For completeness, here's a pair of function components for this:

const CartItem = ({ item, onDelete }) => (
  <Card>
    <Row>
      <Col>
        <h6>{item.title}</h6>
      </Col>
      <Col>
        <h6>USD {item.price}</h6>
      </Col>
      <Col>
        <h6>qty. goes here</h6>
      </Col>
      <Col>
        <ButtonGroup style={{ minWidth: "300px" }}>
          <Button variant="default" size="small">
            -
          </Button>
          <Button variant="default" size="small">
            +
          </Button>
          <Button
            onClick={() => onDelete(item._id)}
            variant="danger"
            size="small"
          >
            DELETE
          </Button>
        </ButtonGroup>
      </Col>
    </Row>
  </Card>
);

const Cart = ({ cart, deleteFromCart }) => {
  const onDelete = React.useCallback(
    (_id) => {
      const cartWithoutGivenId = cart.filter((item) => item._id !== _id);
      const cartAfterDelete = { books: cartWithoutGivenId };
      deleteFromCart(cartAfterDelete);
    },
    [cart],
  );

  if (!cart.length) {
    return <div>Empty Cart</div>;
  }

  return (
    <Card>
      {cart.map((item) => (
        <CartItem key={item._id} item={item} onDelete={onDelete} />
      ))}
    </Card>
  );
};

Upvotes: 2

Anh Tuan
Anh Tuan

Reputation: 1143

 onClick = {this.onClick(cartArr._id)}

change this line to

 onClick = {() => this.onClick(cartArr._id)}

Upvotes: 3

Related Questions