DEV Community

Majuran SIVAKUMAR
Majuran SIVAKUMAR

Posted on

SneakerShop Tutorial : Filter Screen 🔍

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

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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}
      />
    );
  };
Enter fullscreen mode Exit fullscreen mode

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)