Filter products π
We're finally at the point where we can start integrating some really cool features. We'll be adding a filter feature that will make it easier for users to find exactly what they're looking for.
We will create multiple filters, and whenever the user changes one of their values it will update our store
We have
- Sort Filter
- BrandFilter
- Color Filter
- Price Filter
For the Price Filter I wanted a slider to be user friendly. I used
@react-native-community/slider
Letβs create the filter screen first :
import { View, Text, TouchableOpacity, ScrollView, SafeAreaView } from "react-native";
import React, { useLayoutEffect } from "react";
import { useNavigation } from "@react-navigation/native";
import { ChevronLeftIcon, TrashIcon } from "react-native-heroicons/outline";
import SortBy from "../components/Filter/SortBy";
import ColorFilter from "../components/Filter/ColorFilter";
import BrandFilter from "../components/Filter/BrandFilter";
import PriceFilter from "../components/Filter/PriceFilter";
import { useDispatch } from "react-redux";
import { resetFilters } from "../store/features/products/productsSlice";
const FilterScreen = () => {
const navigation = useNavigation();
const dispatch = useDispatch();
useLayoutEffect(() => {
navigation.setOptions({
headerTitle: "Filter",
headerLeft: () => (
<TouchableOpacity onPress={() => navigation.goBack()} className="p-2">
<ChevronLeftIcon color="black" size="30" />
</TouchableOpacity>
),
});
}, []);
const clearFilters = () => {
// Dispatch clear filter action to the store
dispatch(resetFilters());
};
return (
<SafeAreaView className="bg-slate-100">
<TouchableOpacity
className="w-full p-2 pt-4"
onPress={() => clearFilters()}
>
<Text className="text-xs text-right font-semibold">
Clear all filters
<TrashIcon
color="black"
size="18"
style={{ marginLeft: 8 }}
></TrashIcon>
</Text>
</TouchableOpacity>
<View className="h-full">
<ScrollView>
<SortBy />
<ColorFilter />
<BrandFilter />
<PriceFilter />
</ScrollView>
</View>
</SafeAreaView>
);
};
export default FilterScreen;
Store Update
We are going to store ou filters state in product store. And apply the filter on the getProducts selector. With that way we are keeping logic in store.
Letβs update our store with :
// /store/features/products/productsSlice.js
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
items: [],
filters: {},
sort: null,
};
export const productsSlice = createSlice({
name: "products",
initialState,
reducers: {
setProducts: (state, action) => {
state.items = [...action.payload];
},
setFilters: (state, action) => {
/*
ex:
{
"color": {
fn: (e) => e.some(c => ['black', 'yellow'].includes(c))
"black": {
id: "black",
...
},
"yellow": {
id: "yellow",
...
},
}
}
with that structure its easy to find or delete a filter
*/
state.filters = { ...state.filters, ...action.payload };
},
deleteFilterById: (state, action) => {
const filters = { ...state.filters };
const idToDelete = action.payload;
if (filters[idToDelete]) {
delete filters[idToDelete];
}
state.filters = { ...filters };
},
setSort: (state, action) => {
state.sort = action.payload;
},
resetFilters: (state, action) => {
state.filters = {};
state.sort = null;
},
},
});
// Action creators are generated for each case reducer function
export const {
setProducts,
setFilters,
setSort,
deleteFilterById,
resetFilters,
} = productsSlice.actions;
// Selectors
export const selectProducts = ({ products }) => {
const { items, filters, sort } = products;
let productList = [...items];
const filtersIds = Object.keys(filters);
// π CHECK IF THERE IS FILTERS
if (filtersIds.length > 0) {
filtersIds.forEach((filterId) => {
if (filters[filterId]?.fn) {
// APPLY FILTERS
productList = productList.filter(filters[filterId]?.fn);
}
});
}
if (sort?.fn) {
// π SORT PRODUCTS
productList.sort(sort.fn);
}
return productList || [];
};
export const selectSortBy = ({ products }) => products.sort;
export const selectFilters = (id) => (state) => {
// return filters without fn
const { fn, ...filter } = state.products.filters[id] || {};
return filter;
};
export const selectNbOfFilters = (state) =>
Object.keys(state.products.filters).length + (state.products.sort ? 1 : 0);
export default productsSlice.reducer;
Filter Component
import { View, Text, TouchableOpacity } from "react-native";
import React, { useState } from "react";
import { useEffect } from "react";
import Slider from "@react-native-community/slider";
const Filter = ({ filter, defaultValue, onValueChange = () => {} }) => {
const { id, name, type, options } = filter;
const [selectedOptions, setSelectedOptions] = useState({});
useEffect(() => {
defaultValue && setSelectedOptions(defaultValue);
}, [defaultValue]);
const handleFilterChange = (option) => {
let currentOptions = { ...selectedOptions };
if (currentOptions[option?.id]) {
delete currentOptions[option?.id];
} else {
if (type === "radio") {
// reset all options
currentOptions = {};
}
//set new option
currentOptions[option?.id] = option;
}
setSelectedOptions(currentOptions);
onValueChange(currentOptions);
};
return (
<View className="p-2 pb-5 bg-white mt-2 mx-2 rounded" key={id}>
<Text className="text-md font-bold">{name}</Text>
<View className="flex-row flex-wrap w-full justify-center mt-2">
{options &&
options?.map((option) => (
<TouchableOpacity
className={`py-3 px-2 border border-slate-500 w-auto m-1 ${
selectedOptions[option?.id] && "bg-black"
}`}
key={option.id}
onPress={() => handleFilterChange(option)}
>
<Text
className={`text-center ${
selectedOptions[option?.id] && "text-white font-semibold"
}`}
>
{option.name}
</Text>
</TouchableOpacity>
))}
{type === "slider" && (
<View className="w-full text-center">
<Text className="text-center text-md font-bold">
{defaultValue} β¬
</Text>
<Slider
style={{ width: "100%", height: 80 }}
minimumValue={0}
value={defaultValue}
step={10}
maximumValue={200}
minimumTrackTintColor="#000000"
maximumTrackTintColor="#e3e3e3"
onValueChange={onValueChange}
/>
</View>
)}
</View>
</View>
);
};
export default Filter;
Sort Filter
import React from "react";
import Filter from "./Filter";
import { useDispatch, useSelector } from "react-redux";
import {
selectSortBy,
setSort,
} from "../../store/features/products/productsSlice";
import { useState } from "react";
import { useEffect } from "react";
const SORT_FILTERS = {
id: "sort",
name: "Sort by",
type: "radio",
options: [
{
id: "asc",
name: "Price low to hight",
fn: (a, b) => a?.price > b?.price,
},
{
id: "desc",
name: "Price hight to low",
fn: (a, b) => a?.price < b?.price,
},
],
};
const SortBy = () => {
const selectedOptions = useSelector(selectSortBy);
const [defaultValue, setDefaultValue] = useState({});
const dispatch = useDispatch();
const onChange = (selectedValues) => {
const key = Object.keys(selectedValues)[0];
dispatch(setSort(selectedValues[key]));
};
useEffect(() => {
if (selectedOptions) {
setDefaultValue({ [selectedOptions?.id]: selectedOptions });
} else {
setDefaultValue({});
}
}, [selectedOptions]);
return (
<Filter
filter={SORT_FILTERS}
defaultValue={defaultValue}
onValueChange={onChange}
></Filter>
);
};
export default SortBy;
Color Filter
import React from "react";
import Filter from "./Filter";
import { withMultipleChoice } from "./withMultipleChoice";
const generateFilterFunction = (values) => (item) =>
item?.color?.some?.((c) => values.includes(c?.hex));
const COLOR_FILTER = {
id: "color",
name: "Color",
type: "checkbox",
options: [
{
id: "white",
name: "White",
hex: "#ffffff",
},
{
id: "black",
name: "Black",
hex: "#000000",
},
{
id: "gray",
name: "Gray",
hex: "#808080",
},
{
id: "red",
name: "Red",
hex: "#ff0000",
},
{
id: "green",
name: "Green",
hex: "#008000",
},
{
id: "blue",
name: "Blue",
hex: "#0000ff",
},
],
};
export default withMultipleChoice(
COLOR_FILTER,
({ hex }) => hex,
generateFilterFunction
)(Filter);
Brand Filter
import React from "react";
import Filter from "./Filter";
import { withMultipleChoice } from "./withMultipleChoice";
const generateFilterFunction = (values) => (item) =>
values.includes(item.brand);
const BRAND_FILTER = {
id: "brand",
name: "Brand",
type: "checkbox",
options: [
{
id: "nike",
name: "Nike Sportswear",
},
{
id: "adidas",
name: "Adidas Originals",
},
{
id: "new-balance",
name: "New Balance",
},
{
id: "puma",
name: "Puma",
},
],
};
export default withMultipleChoice(
BRAND_FILTER,
({ name }) => name,
generateFilterFunction
)(Filter);
Price Filter
import React from "react";
import Filter from "./Filter";
import { useDispatch, useSelector } from "react-redux";
import {
selectFilters,
setFilters,
} from "../../store/features/products/productsSlice";
const generateFilterFunction =
(value = 200) =>
(a) =>
a.price <= value;
const FILTER = {
id: "price",
name: "Price Max",
type: "slider",
min: 0,
max: 200,
};
const PriceFilter = () => {
const selectedOptions = useSelector(selectFilters(FILTER.id));
const dispatch = useDispatch();
const onChange = (value) => {
const fn = generateFilterFunction(value); // filterFunction
dispatch(setFilters({ [FILTER.id]: { value, fn } }));
};
return (
<Filter
filter={FILTER}
defaultValue={selectedOptions.value}
onValueChange={onChange}
/>
);
};
export default PriceFilter;
We can see that Color and Brand filter are using a higher-order components. Because both components are using same logic of multiple choice.
React HOC is like a helper tool that helps you change the way your React components look or behave without having to rewrite the code every time. It's a way to add extra features or functionality to your components quickly and easily.
withMultipleChoice :
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import {
deleteFilterById,
selectFilters,
setFilters,
} from "../../store/features/products/productsSlice";
export const withMultipleChoice =
(filter, mapValuesFn, filterFn) => (Component) => (props) => {
const selectedOptions = useSelector(selectFilters(filter.id));
const dispatch = useDispatch();
const onChange = (selectedValues) => {
const hasValues = Object.keys(selectedValues).length > 0;
if (!hasValues) {
dispatch(deleteFilterById(filter.id));
return;
}
const values = Object.values(selectedValues).map(mapValuesFn);
const fn = filterFn(values);
dispatch(setFilters({ [filter.id]: { ...selectedValues, fn } }));
};
return (
<Component
filter={filter}
defaultValue={selectedOptions}
onValueChange={onChange}
/>
);
};
Conclusion
In this post, we were able to list products and filter them. We used react HOC to do that. Let's continue to work hard and create something truly incredible together!
Hopefully you enjoyed the article as wellβif so, please let me know in the comment section below!
Source code π¨βπ»
Feel free to checkout the source code there π Github
Top comments (0)