Home > Software engineering >  Filtering with checkboxes in React
Filtering with checkboxes in React

Time:11-27

When I click on the checkboxes, the filter works, but when I click again, the filter does not work and the checkbox flags also seem to work separately. I want to make that when clicked, when the checkbox was worked, filter this. Booleans need to be created for individual checkboxes?

const App = () => {

  const [category, setCategory] = useState(["electronics","jewelery","men's clothing","women's clothing"]);
  const [products, setProducts] = useState([
    {"id":1,"title":"Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops","price":109.95,"category":"electronics", "rating":{"rate":3.9,"count":120}},
    {"id":2,"title":"Mens Casual Premium Slim Fit T-Shirts ","price":22.3, "category":"men's clothing","rating":{"rate":4.1,"count":259}},
    {"id":3,"title":"Mens Cotton Jacket","price":55.99, "category":"men's clothing","rating":{"rate":4.7,"count":500}},
    {"id":4,"title":"Womens Dress","price":15.99, "category":"women's clothing" ,"rating":{"rate":2.1,"count":430}},
    {"id":5,"title":"John Hardy Women's Legends Naga Gold & Silver Dragon Station Chain Bracelet","price":695, "category":"jewelery","rating":{"rate":4.6,"count":400}}
  ])

  let [newProducts, setNewProducts] = useState([])
  let [toCheck, setToCheck] = useState(true);

  const filterProducts = (value) => {
    setToCheck(!toCheck);
    if (toCheck) {
      newProducts = products
    setNewProducts([...newProducts.filter((a) => a.category == value)])
    }
    
  }

  return <div>
    <div className='d-flex justify-content-evenly'>
        {
            category.map((elm, index) => {
                return <div className="form-check ms-2" key={index}>
                    <input className="form-check-input" type="checkbox" value={elm} id="flexCheckDefault" onChange={(e) => filterProducts(e.target.value)}/>
                    <label className="form-check-label" htmlFor="flexCheckDefault">
                        {elm}
                    </label>
                </div>
            })
        }
    </div>
    <div className='d-flex flex-wrap'>
    {
       (newProducts.length == 0 ? products : newProducts).map((prod) => {
        return  <div className='card m-3' style={{ width: "400px" }} key={prod.id}>
        <div className='card-body'>
            <p className='card-text'>Id: {prod.id}</p>
            <h3 className='card-title'>Title: {prod.title}</h3>
            <p className='card-text'>Price: {prod.price}</p>
            <p className='card-text'>Category: {prod.category}</p>
            <p className='card-text'>Rating: {prod.rating.rate}</p>
        </div>
    </div>

      })
    }
    </div>
   </div>
}

Thank you

CodePudding user response:

Simple Solution (with updated data model for category array)

const [category, setCategory] = useState([{value : "electronics", isChecked: false},{value : "jewelery", isChecked: false},{value : "men's clothing", isChecked: false},{value : "women's clothing", isChecked: false}]);
  const [products, setProducts] = useState([
    {"id":1,"title":"Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops","price":109.95,"category":"electronics", "rating":{"rate":3.9,"count":120}},
    {"id":2,"title":"Mens Casual Premium Slim Fit T-Shirts ","price":22.3, "category":"men's clothing","rating":{"rate":4.1,"count":259}},
    {"id":3,"title":"Mens Cotton Jacket","price":55.99, "category":"men's clothing","rating":{"rate":4.7,"count":500}},
    {"id":4,"title":"Womens Dress","price":15.99, "category":"women's clothing" ,"rating":{"rate":2.1,"count":430}},
    {"id":5,"title":"John Hardy Women's Legends Naga Gold & Silver Dragon Station Chain Bracelet","price":695, "category":"jewelery","rating":{"rate":4.6,"count":400}}
  ])

  let [newProducts, setNewProducts] = useState([]);

  const filterProducts = (e) => {
    const categories = [...category];
    const cat = categories.filter((cat)=>cat.value ===  e.target.value);
    cat[0].isChecked = e.target.checked;
    setCategory([...categories]);

    const tempProduct = [];
    for(let c of categories){
      if(c.isChecked){
        tempProduct.push(...products.filter((a) => a.category === c.value));
      }
    }

    setNewProducts([...tempProduct]);
  }

  return <div>
    <div className='d-flex justify-content-evenly'>
        {
            category.map((elm, index) => {
                return <div className="form-check ms-2" key={index}>
                    <input className="form-check-input" type="checkbox" value={elm.value} id="flexCheckDefault" onChange={filterProducts}/>
                    <label className="form-check-label" htmlFor="flexCheckDefault">
                        {elm.value}
                    </label>
                </div>
            })
        }
    </div>
    <div className='d-flex flex-wrap'>
    {
       (newProducts.length === 0 ? products : newProducts).map((prod) => {
        return  <div className='card m-3' style={{ width: "400px" }} key={prod.id}>
        <div className='card-body'>
            <p className='card-text'>Id: {prod.id}</p>
            <h3 className='card-title'>Title: {prod.title}</h3>
            <p className='card-text'>Price: {prod.price}</p>
            <p className='card-text'>Category: {prod.category}</p>
            <p className='card-text'>Rating: {prod.rating.rate}</p>
        </div>
    </div>

      })
    }
    </div>
   </div>

CodePudding user response:

Here is a minimal solution works without useEffect or changing category and products.

A few issues that can be addressed in posted code:

  1. Each input need to have an unique id which matches htmlFor on label for the pair to work together.
  2. When filterProducts sets a new state value, it does not get the updated value in the same block. Only use this event to set the filters in toCheck.
  3. The value type of toCheck can an object of [category]: Boolean so each category can be filtered separately, and filters can be combined as well.
  4. The original products can be filtered before mapped, so there is no need to keep another state of newProducts, or to modify products data.

Example (live demo: stackblitz)

import React, { useState } from 'react';
import './style.css';

export default function App() {
  const [category, setCategory] = useState([
    'electronics',
    'jewelery',
    "men's clothing",
    "women's clothing",
  ]);
  const [products, setProducts] = useState([
    {
      id: 1,
      title: 'Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops',
      price: 109.95,
      category: 'electronics',
      rating: { rate: 3.9, count: 120 },
    },
    {
      id: 2,
      title: 'Mens Casual Premium Slim Fit T-Shirts ',
      price: 22.3,
      category: "men's clothing",
      rating: { rate: 4.1, count: 259 },
    },
    {
      id: 3,
      title: 'Mens Cotton Jacket',
      price: 55.99,
      category: "men's clothing",
      rating: { rate: 4.7, count: 500 },
    },
    {
      id: 4,
      title: 'Womens Dress',
      price: 15.99,
      category: "women's clothing",
      rating: { rate: 2.1, count: 430 },
    },
    {
      id: 5,
      title:
        "John Hardy Women's Legends Naga Gold & Silver Dragon Station Chain Bracelet",
      price: 695,
      category: 'jewelery',
      rating: { rate: 4.6, count: 400 },
    },
  ]);

  const [toCheck, setToCheck] = useState({});

  const filterProducts = (value) =>
    setToCheck((prev) => {
      return { ...prev, [value]: !!!prev[value] };
    });

  return (
    <div>
      <div className="d-flex justify-content-evenly">
        {category.map((elm, index) => {
          return (
            <div className="form-check ms-2" key={index}>
              <input
                className="form-check-input"
                type="checkbox"
                value={elm}
                id={`flexCheckDefault-${index}`}
                onChange={(e) => filterProducts(e.target.value)}
              />
              <label
                className="form-check-label"
                htmlFor={`flexCheckDefault-${index}`}
              >
                {elm}
              </label>
            </div>
          );
        })}
      </div>
      <div className="d-flex flex-wrap">
        {products
          .filter((prod) =>
            Object.keys(toCheck).length === 0 ? true : !!toCheck[prod.category]
          )
          .map((prod) => {
            return (
              <div
                className="card m-3"
                style={{ width: '400px' }}
                key={prod.id}
              >
                <div className="card-body">
                  <p className="card-text">Id: {prod.id}</p>
                  <h3 className="card-title">Title: {prod.title}</h3>
                  <p className="card-text">Price: {prod.price}</p>
                  <p className="card-text">Category: {prod.category}</p>
                  <p className="card-text">Rating: {prod.rating.rate}</p>
                </div>
              </div>
            );
          })}
      </div>
    </div>
  );
}

CodePudding user response:

You can get whether the checkbox is checked through e.target.checked rather than using a state variable. You also don't need to attach a value, you can just pass it directly, not that it matters.

Since you're filtering on multiple elements, this pattern isn't going to work either. You'll need to save all of your filters so you can remove one but keep the others when the box is unchecked. I like to use a Set for this purpose. You can then just filter the products on that set.

You also don't need to use state variables for constants that don't change.

Also, your ids and htmlFors need to be unique, or clicking on a label will always select the first box. It's simpler just to wrap the input with the label so you don't need to worry about it.

Stackblitz: https://stackblitz.com/edit/react-ts-9a9n67?file=App.tsx

const categories = [
  'electronics',
  'jewelery',
  "men's clothing",
  "women's clothing",
];

const allProducts = [
  {
    id: 1,
    title: 'Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops',
    price: 109.95,
    category: 'electronics',
    rating: { rate: 3.9, count: 120 },
  },
  {
    id: 2,
    title: 'Mens Casual Premium Slim Fit T-Shirts ',
    price: 22.3,
    category: "men's clothing",
    rating: { rate: 4.1, count: 259 },
  },
  {
    id: 3,
    title: 'Mens Cotton Jacket',
    price: 55.99,
    category: "men's clothing",
    rating: { rate: 4.7, count: 500 },
  },
  {
    id: 4,
    title: 'Womens Dress',
    price: 15.99,
    category: "women's clothing",
    rating: { rate: 2.1, count: 430 },
  },
  {
    id: 5,
    title:
      "John Hardy Women's Legends Naga Gold & Silver Dragon Station Chain Bracelet",
    price: 695,
    category: 'jewelery',
    rating: { rate: 4.6, count: 400 },
  },
];

export default function App() {
  let [categoryFilters, setcategoryFilters] = useState(new Set());

  function updateFilters(checked, categoryFilter) {
    if (checked)
      setcategoryFilters((prev) => new Set(prev).add(categoryFilter));
    if (!checked)
      setcategoryFilters((prev) => {
        const next = new Set(prev);
        next.delete(categoryFilter);
        return next;
      });
  }

  const filteredProducts =
    categoryFilters.size === 0
      ? allProducts
      : allProducts.filter((p) => categoryFilters.has(p.category));

  return (
    <div>
      <div className="d-flex justify-content-evenly">
        {categories.map((elm, index) => {
          return (
            <div className="form-check ms-2" key={index}>
              <label className="form-check-label">
                <input
                  className="form-check-input"
                  type="checkbox"
                  onChange={(e) => updateFilters(e.target.checked, elm)}
                />
                {elm}
              </label>
            </div>
          );
        })}
      </div>
      <div className="d-flex flex-wrap">
        {filteredProducts.map((prod) => {
          return (
            <div className="card m-3" style={{ width: '400px' }} key={prod.id}>
              <div className="card-body">
                <p className="card-text">Id: {prod.id}</p>
                <h3 className="card-title">Title: {prod.title}</h3>
                <p className="card-text">Price: {prod.price}</p>
                <p className="card-text">Category: {prod.category}</p>
                <p className="card-text">Rating: {prod.rating.rate}</p>
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
}
  • Related