Search code examples
javascriptreactjstypescriptreact-typescript

React Typescript fire onClick button event using .bind


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.


Solution

  • 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.