Фильтрация с помощью флажков в React

Когда я нажимаю на флажки, фильтр работает, но когда я нажимаю снова, фильтр не работает, и флаги флажков, похоже, также работают отдельно. Я хочу сделать так, чтобы при нажатии, когда флажок сработал, отфильтровать это. Булевы значения нужно создавать для отдельных флажков?

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>
}

Спасибо

🤔 А знаете ли вы, что...
React обладает активным сообществом разработчиков и множеством сторонних библиотек и компонентов.


54
3

Ответы:

Решено

Вы можете узнать, установлен ли флажок, с помощью e.target.checked, а не с помощью переменной состояния. Вам также не нужно прикреплять значение, вы можете просто передать его напрямую, но это не имеет значения.

Поскольку вы фильтруете несколько элементов, этот шаблон также не будет работать. Вам нужно будет сохранить все ваши фильтры, чтобы вы могли удалить один, но оставить остальные, когда флажок не установлен. Мне нравится использовать Set для этой цели. Затем вы можете просто отфильтровать продукты в этом наборе.

Вам также не нужно использовать переменные состояния для констант, которые не изменяются.

Кроме того, ваши id и htmlFor должны быть уникальными, иначе нажатие на метку всегда будет выбирать первое поле. Проще просто обернуть ввод меткой, поэтому вам не нужно об этом беспокоиться.

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>
  );
}

Простое решение (с обновленной моделью данных для массива категорий)

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>

Вот минимальное решение работает без useEffect или изменения category и products.

Несколько проблем, которые можно решить в опубликованном коде:

  1. У каждого input должен быть уникальный id, который соответствует htmlFor на label, чтобы пара работала вместе.
  2. Когда filterProducts устанавливает новое значение состояния, оно не получает обновленное значение в том же блоке. Используйте это событие только для установки фильтров в toCheck.
  3. Тип значения toCheck может быть объектом [category]: Boolean, поэтому каждую категорию можно фильтровать отдельно, а также фильтры можно комбинировать.
  4. Исходный products можно отфильтровать перед сопоставлением, поэтому нет необходимости сохранять другое состояние newProducts или изменять products данные.

Пример (живая демонстрация: 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>
  );
}