0

I have this code i'm running to filter my array and prompt checkboxes based on one of the items (here brand) The problem is, i want to have multiple set of checkboxes for other items as well (say, series for the sake of exemple) which all work together. For some reason i can't make the code work here on the snippet, but here it is :

const Item = ({ title }) => <div><h4>{title}</h4></div>;

const Filter = ({ value, active, onChange }) => (
  <label className="filter">
    <input type="checkbox" checked={active} data-value={value} onChange={onChange} />
    {value}
  </label>
);


function App({ items }) {
  const [ filters, setFilters ] = React.useState([]);

  React.useEffect(() => {
    const filterValues = [...new Set([ 'all', ...items.map(n => n.brand) ])];
    setFilters(filterValues.map((n, i) => ({ active: false, value: n, id: i + 1 })));
  }, [ items ]);

  const onFilterChange = ({ target: { checked: active, dataset: { value } } }) => {
    const
      newFilters = filters.map(n => [ n.value, 'all' ].includes(value) ? { ...n, active } : n),
      isAll = newFilters.filter(n => n.value !== 'all').every(n => n.active);

    newFilters.find(n => n.value === 'all').active = isAll;

    setFilters(newFilters);
  };

  const
    filteredBrands = filters.filter(n => n.active).map(n => n.value),
    filteredItems = items.filter(n => filteredBrands.includes(n.brand));

  return (
    <div>
      {filters.map(n => <Filter key={n.id} {...n} onChange={onFilterChange} />)}
      {filteredItems.map(n => <Item key={n.id} {...n} />)}
    </div>
  );
}

const Items = [
  { brand: 'Acer', title: 'Acer 1', series: "1" },
  { brand: 'Acer', title: 'Acer 2', series: "1" },
  { brand: 'Asus', title: 'Asus 1', series: "3"  },
  { brand: 'Acer', title: 'Acer 3', series: "1" },
  { brand: 'Apple', title: 'Apple 1', series: "3"  },
  { brand: 'Acer', title: 'Acer 4', series: "3" },
  { brand: 'Apple', title: 'Apple 2', series: "1" },
  { brand: 'Asus', title: 'Asus 2', series: "2" },
  { brand: 'Asus', title: 'Asus 3', series: "2" },
  { brand: 'Acer', title: 'Acer 5', series: "2" },
  { brand: 'Apple', title: 'Apple 3', series: "2" },
].map((n, i) => ({ ...n, id: i + 1 }));

ReactDOM.render(<App items={Items} />, document.getElementById('app'));
 

Right now this code allows me to filter objects by "brand" but i also want to filter them by "series". Can't figure it out, any help please? Thank you!

1 Answer 1

1

Start by breaking down the problem into smaller parts.

Your data structure is:

const items = [
  { id: 1, brand: 'Acer', title: 'Acer 1', series: "1" },
  { id: 2, brand: 'Acer', title: 'Acer 2', series: "1" },
  { id: 3, brand: 'Asus', title: 'Asus 1', series: "3"  },
  { id: 4, brand: 'Acer', title: 'Acer 3', series: "1" },
  { id: 5, brand: 'Apple', title: 'Apple 1', series: "3"  },
  { id: 6, brand: 'Acer', title: 'Acer 4', series: "3" },
  { id: 7, brand: 'Apple', title: 'Apple 2', series: "1" },
  { id: 8, brand: 'Asus', title: 'Asus 2', series: "2" },
  { id: 9, brand: 'Asus', title: 'Asus 3', series: "2" },
  { id: 10, brand: 'Acer', title: 'Acer 5', series: "2" },
  { id: 11, brand: 'Apple', title: 'Apple 3', series: "2" },
]

You want to filter it based on brand and series. So that'd by 2 filter functions. Finally, you need a list of filtered ids/objects. I'd suggest that you convert this into an object, but we can do it with an array too.

The filter functions would look something like:

const filterByBrand = (selectedBrands, allItems) => allItems.filter((item) => selectedBrands.indexOf(item.brand) > -1)

const filterBySeries = (selectedSeries, allItems) => allItems.filter((item) => selectedSeries.indexOf(item.series) > -1)

Now these can be used in a useEffect:

const [items, setItems] = useState(allItems)
const [filteredItems, setFilteredItems] = useState(allItems)
const [selectedBrands, setSelectedBrands] = useState([])
const [selectedSeries, setSelectedSeries] = useState([])

useEffect(() => {
  let filtered = filterByBrands(selectedBrands, items) // first filtering by brands
  filtered = filterBySeries(selectedSeries, filtered) // then filtering the results by series
  setFilteredItems(filtered) // finally, setting it as a state variable
}, [items, selectedBrands, selectedSeries])

At this point, we have decoupled the filtering logic. Now we can easily add logic to modify the selectedBrands and selectedSeries.

Bonus

This part is not tested, so you might have to do some refactoring. Our checkbox could look like:

const Checkbox = ({ onClick, name }) => {
  const [selected, setSelected] = useState(false)
  const onCheckboxClick = () => {
    setSelected(prev => !prev)
    onClick()
  }
  return <input
            name={name}
            type="checkbox"
            checked={selected}
            onChange={onCheckboxClick} />
}

Here we are lifting the state up.

Next, we need to render these checkboxes for brands and series.

const BrandCheckboxes = ({ brands, setSelected }) => {
  const onBrandCheckboxClick = (brand) => {
    setSelected(prev => prev.indexOf(brand) > -1? [...prev.filter(b => b !== brand)] : [...prev, brand])
  }

  return 
    brands.map(brand => <Checkbox key={brand} name={brand} onClick={() => onBrandCheckboxClick(brand)} />)
  
}

The same thing can be done for series.

Finally, we can do something like:

const allItems = [
  { id: 1, brand: 'Acer', title: 'Acer 1', series: "1" },
  { id: 2, brand: 'Acer', title: 'Acer 2', series: "1" },
  { id: 3, brand: 'Asus', title: 'Asus 1', series: "3"  },
  { id: 4, brand: 'Acer', title: 'Acer 3', series: "1" },
  { id: 5, brand: 'Apple', title: 'Apple 1', series: "3"  },
  { id: 6, brand: 'Acer', title: 'Acer 4', series: "3" },
  { id: 7, brand: 'Apple', title: 'Apple 2', series: "1" },
  { id: 8, brand: 'Asus', title: 'Asus 2', series: "2" },
  { id: 9, brand: 'Asus', title: 'Asus 3', series: "2" },
  { id: 10, brand: 'Acer', title: 'Acer 5', series: "2" },
  { id: 11, brand: 'Apple', title: 'Apple 3', series: "2" },
]
const filterByBrand = (selectedBrands, items) => items.filter((item) => selectedBrands.indexOf(item.brand) > -1)

const filterBySeries = (selectedSeries, items) => items.filter((item) => selectedSeries.indexOf(item.series) > -1)

const FilteredItems = () => {
  const [items, setItems] = useState(allItems)
  const [filteredItems, setFilteredItems] = useState(allItems)
  const [selectedBrands, setSelectedBrands] = useState([])
  const [selectedSeries, setSelectedSeries] = useState([])

  const [brands, setBrands] = useState([...new Set(allItems.map(item => item.brand))])
  const [series, setSeries] = useState([...new Set(allItems.map(item => item.series))])

  useEffect(() => {
    let filtered = filterByBrands(selectedBrands, items) // first filtering by brands
    filtered = filterBySeries(selectedSeries, filtered) // then filtering the results by series
    setFilteredItems(filtered) // finally, setting it as a state variable
  }, [items, selectedBrands, selectedSeries])

  return (
    <>
      <BrandCheckboxes brands={brands} setSelected={setSelectedBrands} />
      <SeriesCheckboxes brands={brands} setSelected={setSelectedBrands} />
      {filteredItems.map((item) => (
        <div key={item.id}>
          <pre> 
            {JSON.stringify(item, null, 2)}
          </pre>
        </div>
      ))}
    </>
  )

}

Update:

The filter in useEffect is like an AND gate, it will only give you items which match both filter. If you want an OR gate, or if you want items which match either of the filters, you would do something like:

useEffect(() => {
  const filteredByBrands = filterByBrands(selectedBrands, items) // first filtering by brands
  const filteredBySeries = filterBySeries(selectedSeries, items) // then filtering the results by series
  const filteredIds = [...filteredByBrands.map(i => i.id), ...filteredBySeries.map(i => i.id)]
  const uniqueIds = [...new Set(filteredIds)]
  const filtered = uniqueIds.map(id => items[id - 1]) // since you incremented the ids
  
  setFilteredItems(filtered) // finally, setting it as a state variable
}, [items, selectedBrands, selectedSeries])
Sign up to request clarification or add additional context in comments.

3 Comments

Thank you for the detailed rundown!
Sorry to bother again but i made the code work fine but currently for exemple if for the "Acer 1" to appear i don't have Brand "Acer" and Series "1" both checked it won't appear. I would like to make it so that if at least one of the parameters is checked the item will appear (not need all of the boxes checked). For exemple : only check the "Series 1" box and it will give me all Series 1 items regardless of Brand. Is that possible? I'm messing with the useEffect and so close to do it, but can't figure it out. Thank you.
In this case you just need to tweak the useEffect. I'll update the answer.

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.