SneakerShop Tutorial : Filter Screen 🔍
Published: 2023-07-07 17:00:00



## 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. 


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0ls7gmtfhpgzvzmrrw8d.png)

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 : 

```jsx
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 :

```jsx
// /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

```jsx
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

```jsx
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

```jsx
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

```jsx
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

```jsx
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 : 

```jsx
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**](https://github.com/Maj07/sneaker-app-react-native)