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">× {category}</span>
</button>
))
: null}
{props.selectedBrand ? (
<button onClick={() => props.handleFilterChange("brand", null)}>
<span aria-hidden="true">× {props.selectedBrand}</span>
</button>
) : null}
{props.selectedColor ? (
<button onClick={() => props.handleFilterChange("color", null)}>
<span aria-hidden="true">× {props.selectedColor}</span>
</button>
) : null}
</div>
</>
);
}
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;
});
}