Select multiple buttons and adding removal icon if more than one selected - React Hooks

Viewed 1008

I'm making a "filter by category"-type function and for that I need to be able to select multiple options. While I understand that I need to store the selection in the state, I'm hitting a wall when I try to check if the current state already includes the button I am clicking.

Also, pt.II of this question is whether there's a way for the already selected button to "know" whether any of the other buttons have been selected, as the appearance of the buttons need to change depending on whether a single or multiple buttons are selected.

Any help / advice is greatly appreciated.

Code:

import React, { useState } from 'react';
import PropTypes from 'prop-types';
import styles from './styles.module.sass';

const Category = (props) => {
  const { categories } = props;
  const [selected, setSelected] = useState([]);

  const handleSelected = (button) => {
    if (selected.includes(button)) {
      setSelected({ selected: selected.filter((s) => s !== button) });
    } else {
      setSelected(...selected, [button]);
    }
  };

  return (
    <div className={styles.category}>
      {categories.map((category) => (
        <button
          type='button'
          key={category.id}
          onClick={handleSelected(category.id)}
          className={selected.includes(category.id) ? styles.selected : styles.button}
        >
          {category.name}
        </button>
      ))}
    </div>
  );
};

Category.defaultProps = {
};

Category.propTypes = {
  categories: PropTypes.arrayOf(PropTypes.object).isRequired,
};

export default Category;

1 Answers

There some mistakes in your code first your onClick hander is called every render and not when you click the button: onClick={handleSelected(category.id)}. if you want the function to be called when you click, you need to send arrow function: onClick={() => handleSelected(category.id)}

second, the add selected has few bugs:

  const handleSelected = (button) => {
    if (selected.includes(button)) {
      setSelected({ selected: selected.filter((s) => s !== button) }); // selected is not an object its array of selected ids
    } else { 
      setSelected(...selected, [button]); // button is ignored setState accepts only one argument 
    }
  };

you can fix it by doing this (I also changed to update logic to use a updater form of setState):

  const handleSelected = (id) => {
    if (selected.includes(id)) {
      setSelected(prevSelected => prevSelected.filter((s) => s !== id));
    } else { 
      setSelected(prevSelected => ([...prevSelected, id]));
    }
  };

also note it may be better to store the selected state inside to category array so you wont have to use selected.includes every time and just use boolean instead.

categories = [
{
   name: 'one',
   id: 1,
   isSelected: true
}, 
{
   name: 'two',
   id: 2,
   isSelected: false,
}
]
Related