1

I have the following Cart.tsx code with two functions onRemove and onAdd with .bind() passing some data:

const Cart = (props: CartProps) => {
  // ...

  const cartItemRemoveHandler = (id: string) => {    
    console.log("CartItemRemoveHandler");
  };

  const cartItemAddHandler = (item: CartItemProps) => {
    console.log("CartItemAddHandler");
  };

  const cartItems = (
    <ul className={classes["cart-items"]}>
      {cartCtx.items.map((item) => (
        <CartItem
          key={item.id}
          id={item.id}
          name={item.name}
          amount={item.amount}
          price={item.price}
          onRemove={cartItemRemoveHandler.bind(null, item.id)}
          onAdd={cartItemAddHandler.bind(null, item)}
        />
      ))}
    </ul>
  );
  
  // ...
};

CartItem.tsx:

export interface CartItemProps {
  id: string;
  name: string;
  amount: number;
  price: number;
  onAdd?: (id: string) => void;
  onRemove?: (item: CartItemProps) => void;
}

const CartItem = (props: CartItemProps) => {
  const price = `$${props.price.toFixed(2)}`;

  return (
    <li className={classes["cart-item"]}>
      <div>
        <h2>{props.name}</h2>
        <div className={classes.summary}>
          <span className={classes.price}>{price}</span>
          <span className={classes.amount}>x {props.amount}</span>
        </div>
      </div>
      <div className={classes.actions}>
        <button onClick={props.onRemove}>-</button>
        <button onClick={props.onAdd}>+</button>
      </div>
    </li>
  );
};

The error occurs at CartItem.tsx at the onClick functions in the buttons. The onClick is red underlined with the following error:

(JSX attribute) React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefined

Type '((id: string) => void) | undefined' is not assignable to type 'MouseEventHandler<HTMLButtonElement> | undefined'.
  Type '((id: string) => void' is not assignable to type 'MouseEventHandler<HTMLButtonElement>'.
    Types of parameters 'id' and 'event' are incompatible.
      Type 'MouseEvent<HTMLButtonElement, MouseEvent>' is not assignable to type 'string'
The expected type comes from property 'onClick' which is declared here on type 'DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>'

I don't need the event properties. I just want the function to execute whenever the button is clicked. Can I work around this? Help is appreciated.

Update: Solved the error by changing the interface onAdd and onRemove declarations to: onAdd?: () => void; and onRemove?: () => void;. Essentially, I just removed the parameters from the functions in the interface, which results in the following:

export interface CartItemProps {
  id: string;
  name: string;
  amount: number;
  price: number;
  onAdd?: () => void;
  onRemove?: () => void;
}

The .bind() functions are handling the arguments. There is no need to define them in the interface as well, which was my mistake.

2
  • use arrow function onClick={() => props.onRemove} Commented Jan 13, 2022 at 14:33
  • @MohsenMahoski When I do that, the Handlers are not firing Commented Jan 13, 2022 at 14:35

2 Answers 2

2

use arrow function in onClick:

export interface CartItemProps {
    id: string;
    name: string;
    amount: number;
    price: number;
    onAdd?: (id: string) => void;
    onRemove?: (item: CartItemProps) => void;
  }
  
  const CartItem = (props: CartItemProps) => {
    const price = `$${props.price.toFixed(2)}`;
  
    const item = { 
        id: props.id,
        name: props.name,
        amount: props.amount,
        price: props.price,
    } as CartItemProps;
    return (
      <li className={classes["cart-item"]}>
        <div>
          <h2>{props.name}</h2>
          <div className={classes.summary}>
            <span className={classes.price}>{price}</span>
            <span className={classes.amount}>x {props.amount}</span>
          </div>
        </div>
        <div className={classes.actions}>
          <button onClick={() => props.onRemove?.(item)}>-</button>
          <button onClick={() => props.onAdd?.(item.id)}>+</button>
        </div>
      </li>
    );
  };
Sign up to request clarification or add additional context in comments.

5 Comments

Thank you, that worked. However, now I have to define item and item.id at 2 places: CartItem.tsx in the button elements and in Cart.tsx in the .bind functions. Is that conventional? I prefer having it at just 1 place.
onAdd and onRemove could have their types simplified to ()=>void as the bind is handling the arguments anyway.
@samthecodingman Thanks! The only thing I had to do was remove the parameters in the onAdd and onRemove from CartItemProps interface. Now the item and item.id are only defined in the .bind functions, great!
@SangeetAgarwal As of right now, you can look at my code in the OP, with the addition of changing the interface onAdd and onRemove declarations to: onAdd?: () => void; and onRemove?: () => void;. Essentially, I just removed the parameters from the functions in the interface.
@samthecodingman If you can provide an actual answer I will accept it. I've updated the OP with the solution.
1

Focussing on this line of your error chain:

Type '((id: string) => void) | undefined' is not assignable to type 'MouseEventHandler<HTMLButtonElement> | undefined'.

This error is stating that what you have configured as the type of the onAdd hook, is incompatible with an onClick handler that accepts a MouseEvent<...> object as the first argument.

However, because you are binding your id and item inside of the Cart element, you are actually passing functions with the type () => void down to CartItem instead, which is different to your interface declaration anyway. These () => void functions are compatible with onClick handlers because they ignore any arguments passed to them.

Therefore, you can fix the issue by updating your interface to just:

export interface CartItemProps {
  id: string;
  name: string;
  amount: number;
  price: number;
  onAdd?: () => void;
  onRemove?: () => void;
}

This allows you to continue using the following pieces as-is:

// Cart
onRemove={cartItemRemoveHandler.bind(null, item.id)}
onAdd={cartItemAddHandler.bind(null, item)}
// CartItem
<button onClick={props.onRemove}>-</button>
<button onClick={props.onAdd}>+</button>

However, if in the future cartItemAddHandler or cartItemRemoveHandler have more parameters added to them, and you don't bind all of the arguments of the function handlers properly, you will start getting MouseEvent<...> objects passed through to your function unexpectedly.

// Cart
cartItemRemoveHandler = (id: string, quantity: number) => { ... }
/* ... */
onRemove={cartItemRemoveHandler.bind(null, item)}
// CartItem
export interface CartItemProps
  /* ... */
  onRemove?: () => void;
  /* ... */
}
/* ... */
<button onClick={props.onRemove}>-</button>

At runtime, when onAdd is fired, quantity here would be given the MouseEvent<...>, not a number.

You can prevent mistakes like this by updating the interface to accept MouseEventHandler<HTMLButtonElement> objects so that TypeScript appropriately reports the error when you don't bind the handlers properly.

export interface CartItemProps {
  id: string;
  name: string;
  amount: number;
  price: number;
  onAdd?: MouseEventHandler<HTMLButtonElement>;
  onRemove?: MouseEventHandler<HTMLButtonElement>;
}

In addition, you can swap the following lines to prevent the MouseEvent being passed in the wrong spot:

cartItemRemoveHandler.bind(null, item)

for:

() => cartItemRemoveHandler(item)
// or
(ev: MouseEvent<...>) => cartItemRemoveHandler(item) // no need to explicitly type the `ev` as a MouseEvent here, it will be inferred from the interface.

Side note: Even with these changes, the handler you use for onAdd is accepting an item object, but onRemove receives a string. This is backwards in comparison to your original interface.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.