1

I am currently making a product filter website with react-router. I am able to get the searchParams, for example "color: black" but as soon as I select a different color it replaces the current selected value, but I would like to set multiple values example: black, grey, etc.

I tried with searcgParams.getAll but couldn't get it to work.

import { useState } from "react";
import { Link, useSearchParams, useLoaderData } from "react-router-dom";
import FilterNav from "../../components/FilterNav";
import { getProducts } from "../../api";

export function loader() {
  return getProducts();
}

export default function Products() {
  const [searchParams, setSearchParams] = useSearchParams();
  // const [selectedCategory, setSelectedCategory] = useState(
  //   searchParams.get("category")
  // );
  // const [selectedBrand, setSelectedBrand] = useState(searchParams.get("brand"));
  // const [selectedColor, setSelectedColor] = useState(searchParams.get("color"));

  const [error, setError] = useState(null);

  const products = useLoaderData();

  const uniqueCategories = [
    ...new Set(products.map((product) => product.category)),
  ];
  const uniqueBrands = [...new Set(products.map((product) => product.brand))];
  const uniqueColors = [...new Set(products.map((product) => product.color))];
  const uniquePrices = [...new Set(products.map((product) => product.price))];

  const selectedCategory = searchParams.get("category");
  const selectedBrand = searchParams.get("brand");
  const selectedColor = searchParams.get("color");

  function handleFilterChange(key, value) {
    setSearchParams((prevParams) => {
      let values = prevParams.get(key)?.split(",");

      if (values) {
        // existing values, add/remove specific values
        if (values.includes(value)) {
          // remove value from array
          values = values.filter((currentValue) => currentValue !== value);
        } else {
          // append value to array
          values.push(value);
        }

        if (!!values.length) {
          // set new key-value if values is still populated
          prevParams.set(key, values);
        } else {
          // delete key if values array is empty
          prevParams.delete(key);
        }
      } else {
        // no values for key, create new array with value
        prevParams.set(key, [value]);
      }
      if (value === null) {
        prevParams.delete(key);
      }

      return prevParams;
    });
  }

  // function handleFilterChange(key, value) {
  //   setSearchParams((prevParams) => {
  //     if (value === null) {
  //       prevParams.delete(key);
  //     } else {
  //       prevParams.set(key, value);
  //     }
  //     return prevParams;
  //   });
  // }

  const filteredProducts = products.filter((product) => {
    const filteredBrand =
      !selectedBrand || selectedBrand.includes(product.brand);
    const filteredCategory =
      !selectedCategory || selectedCategory.includes(product.category);
    const filteredColor =
      !selectedColor || selectedColor.includes(product.color);
    return filteredBrand && filteredCategory && filteredColor;
  });

  const allProducts = filteredProducts.map((product) => (
    <div key={product.id} className="product-tile">
      <Link
        to={product.id}
        state={{
          search: `?${searchParams.toString()}`,
          category: selectedCategory,
        }}
      >
        <h2>
          {product.brand} {product.name}
        </h2>
        <img src={product.image} />
        <div className="product-info">
          <p>
            {product.category} - {product.color} - ${product.price}{" "}
          </p>
        </div>
      </Link>
    </div>
  ));

  if (error) {
    return <h1>There was an error: {error.message}</h1>;
  }

  return (
    <div className="product-list-container">
      <FilterNav
        categoryOptions={uniqueCategories}
        brandOptions={uniqueBrands}
        colorOptions={uniqueColors}
        selectedCategory={selectedCategory}
        selectedBrand={selectedBrand}
        selectedColor={selectedColor}
        handleFilterChange={handleFilterChange}
      />
      <div className="product-list">{allProducts}</div>
    </div>
  );
}


import { useState } from "react";
export default function FilterNav(props) {
  // const [selectedCategories, setSelectedCategories] = useState([]);

  // const handleCategoryClick = (category) => {
  //   if (selectedCategories.includes(category)) {
  //     setSelectedCategories(selectedCategories.filter((c) => c !== category));
  //     props.handleFilterChange("category", null); // Remove the category filter
  //   } else {
  //     setSelectedCategories([...selectedCategories, category]);
  //     props.handleFilterChange("category", category); // Apply the category filter
  //   }
  // };

  const renderFilterList = (options, key) => {
    return options.map((value, id) => {
      return (
        <a onClick={() => props.handleFilterChange(key, value)} key={id}>
          {value}
        </a>
      );
    });
  };

  return (
    <>
      <div className="product-list-filter-buttons">
        <div className="dropdown">
          <button className="dropbtn">Brands</button>
          <div className="dropdown-content">
            {renderFilterList(props.brandOptions, "brand")}
          </div>
        </div>

        <div className="dropdown">
          <button className="dropbtn">Categories</button>
          <div className="dropdown-content">
            {renderFilterList(props.categoryOptions, "category")}
          </div>
        </div>

        <div className="dropdown">
          <button className="dropbtn">Colors</button>
          <div className="dropdown-content">
            {renderFilterList(props.colorOptions, "color")}
          </div>
        </div>
        {props.selectedCategory ||
        props.selectedBrand ||
        props.selectedColor ? (
          <button
            onClick={() => {
              props.handleFilterChange("category", null);
              props.handleFilterChange("brand", null);
              props.handleFilterChange("color", null);
            }}
            className="product-type clear-filters"
          >
            Clear all filters
          </button>
        ) : null}
      </div>

      <div>
        {props.selectedCategory
          ? props.categoryOptions.map((category, index) => (
              <button
                key={index}
                onClick={() => props.handleFilterChange("category", category)}
              >
                <span aria-hidden="true">&times; {category}</span>
              </button>
            ))
          : null}

        {props.selectedBrand ? (
          <button onClick={() => props.handleFilterChange("brand", null)}>
            <span aria-hidden="true">&times; {props.selectedBrand}</span>
          </button>
        ) : null}
        {props.selectedColor ? (
          <button onClick={() => props.handleFilterChange("color", null)}>
            <span aria-hidden="true">&times; {props.selectedColor}</span>
          </button>
        ) : null}
      </div>
    </>
  );
}

bukke
  • 13
  • 4

1 Answers1

0

Instead of using searchParams.set method which replaces existing key-value entries you likely want to use the searchParams.append which will add multiple keys for the values.

Example:

function handleFilterChange(key, value) {
  setSearchParams((prevParams) => {
    if (value === null) {
      prevParams.delete(key);
    } else {
      prevParams.append(key, value); // <-- append key-value pair
    }
    return prevParams;
  });
}

Then using searchParams.getAll will return an array of values associated with the specific key.

Example, if the search string is something like `"...?color=black+color=grey"

searchParams.getAll("color"); // ["black", "grey"]

The above approach is a bit of an all-or-nothing solution though, e.g. using searchParams.delete("color") will remove all color queryString parameters. If you want more fine-grained control over individual color selections then you'll need to manage this yourself manually.

The following example should be close to what you may be looking for for individual parameter:

function handleFilterChange(key, value) {
  setSearchParams((prevParams) => {
    let values = prevPrams.get(key)?.split(",");

    if (values) {
      // existing values, add/remove specific values
      if (values.includes(value) {
        // remove value from array
        values = values.filter(currentValue => currentValue !== value);
      } else {
        // append value to array
        value.push(value);
      }

      if (!!values.length) {
        // set new key-value if values is still populated
        prevParams.set(key, values);
      } else {
        // delete key if values array is empty
        prevParams.delete(key);
      }
    } else {
      // no values for key, create new array with value
      prevParams.set(key, [value]);
    }

    return prevParams;
  });
}
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • Thank you, i managed to get it working with your example. now i am having issues that i can't seem to figure out In FilterNav i have dropdown buttons "category" "brands" etc.. the dropdown list is all the uniquesValues, when a value is clicked it renders another button under the menu with the current selectedValue so users can see current filter and can remove by clicking the button i have tried it on the Category by mapping over the unique values which works but when i click a value it renders all the category value buttons instead. how can i only render the button value that is clicked? – bukke Jun 21 '23 at 12:31
  • i edited the code above to the current version – bukke Jun 21 '23 at 12:36
  • @bukke You could probably map the `selectedCategory` array instead of the full `categoryOptions` array, e.g. `(props.selectedCategory ?? []).map((category) => )`. Does this make sense? – Drew Reese Jun 21 '23 at 15:14
  • I see! That was my first idea to map over the selectedCategory but i got a error that i can’t map selectedCategory. Maybe i did something wrong there. I don’t understand the empty array [] part.. :) – bukke Jun 21 '23 at 15:20
  • @bukke Up where you get the query parameters, you need to get ***all*** of them, e.g. `const selectedCategory = searchParams.getAll("category");`, which should return an array that is mappable. `props.selectedCategory ?? []` is just a way to provide a defined fallback array value in case `props.selectedCategory` happens to be null or undefined. – Drew Reese Jun 21 '23 at 15:22
  • Oh yeah right i forgot the getAll. That makes sense! I’ll give it a shot, thank you so much for your help :) – bukke Jun 21 '23 at 15:31