ReactHustle

How to Implement Multiple Filters in React

Jasser Mark Arioste

Jasser Mark Arioste

How to Implement Multiple Filters in React

Hello, hustlers! In this tutorial, you'll learn how to implement multiple filters in React. We'll use NextJS with Typescript to implement this feature.

The Problem #

Suppose you're building an eCommerce app and you have multiple components that filter some products. But each component has different functionality, one is for search, one is for category, one is for branch, one is for price range, etc. 

How do you represent the filter state so that even if you add, modify or remove filter components, the filter state representation stays the same?

The Solution #

Filtering usually has two steps, first is identifying the filter conditions and then using the filter conditions to an existing collection of elements.

To identify the filter conditions, we'll create a <FilterProvider/> component that uses the React.Context API and a useFilters custom hook to easily add or remove filter conditions.

Next, we'll use these filter conditions by calling a fetch request to the backend and rendering the filtered data on the frontend.

A filter condition usually has three parts: property, operator, and value. I suggest representing a filter state using these three. For example, if you want to filter all products in the shoes category and the price is greater than $100 but less than $200. 

To represent this as a state, I would use something like this:

export enum Operator {
  equals = "=",
  greater_than = ">",
  less_than = "<",
}
export type FilterValue = string | number | boolean | Date;
export type FilterCondition = [string, Operator, FilterValue];

export const filterState: FilterCondition[] = [
  ["category", Operator.equals, "shoes"],
  ["price", Operator.greater_than, 100],
  ["price", Operator.less_than, 200],
];
12345678910111213

Tutorial Objectives #

Here are our step-by-step objectives to create a multi-filter feature in React.

  1. Create the <FilterProvder/> Component using the React.Context API
  2. Create the custom hook useFilters()
  3. Create <CategoryFilter/> component 
  4. Create a backend API endpoint
  5. Add filter implementation to the backend endpoint by using the filter input
  6. Integrate backend and frontend

Final Output #

Here's the final output of what we'll be making today:

React Multiple Filters&nbsp; - Final Output

Step 0: Project Setup #

First, let's create a new NextJS Project by running the following command:

npx create-next-app --ts --app react-multiple-filters

This creates a new NextJS project inside the /react-multiple-filters folder. The --ts option initializes the project to use Typescript and will create all files  to use .ts and .tsx extension.

The --app option will initialize it as an /app router project as opposed to the old /pages directory.

To start the local dev environment, simply run the following commands:

cd react-multiple-filters
yarn dev 
# or
npm run dev

Step 1: Preparing the Data #

In this step, we'll use faker to generate a products.json file to act as our database. First, install @faker-js/faker package using the command:

yarn add @faker-js/faker
1

Next, create the file seed_db.js in the root directory of your project.

const { faker } = require("@faker-js/faker");
const { writeFileSync } = require("fs");

async function seed() {
  // generate 100 products
  const products = Array.from(new Array(100), () => {
    const price = parseInt(
      faker.commerce.price({ dec: 0, max: 1000, min: 50 })
    );
    return {
      productName: faker.commerce.productName(),
      category: faker.commerce.department(),
      price: price,
      description: faker.commerce.productDescription(),
      material: faker.commerce.productMaterial(),
    };
  });
  writeFileSync("./products.json", JSON.stringify(products, null, 4));
}

seed();
123456789101112131415161718192021

Next, run the script using the following command:

node seed_db.js

You should see a new products.json file on the root directory of your project.

Step 2: Creating a Backend endpoint #

In this step, we'll create a backend endpoint that uses an in-memory database (@seald/nedb). The nedb package is an alternative to MongoDB which is good for tutorial purposes.

First, let's install the @seald/nedb package by running the command:

yarn add @seald/nedb

Next, create the file /app/api/products/route.ts:

// app/api/products/route.ts
import DataStore from "@seald-io/nedb";
import products from "@/products.json";
import { NextResponse } from "next/server";

// create a new in-memory database
const db = new DataStore();
// insert all products into the database
const documents = products.map((prod) => {
  return db.insertAsync(prod);
});

export async function GET(req: Request, {}) {
  await documents;
  const products = await db.find({});
  return NextResponse.json(products);
}
1234567891011121314151617

In the above code, we're importing the products.json file and iterate through each product before inserting it into the database.

After this step, you can go to localhost:3000/api/products, and you should get the following result:

React multiple filters products API

Step 3: Creating the <FilterProvider/> #

The <FilterProvider/> will control the filter state of parts of the application. By using the React Context API, we can have access to the filter state inside any component, this makes it easy to add components that use or modify the filter state.

First, create the filter components/FilterProvider.tsx.

// components/FilterProvider.tsx

import { PropsWithChildren, createContext, useContext, useState } from "react";
// define filter operators
export enum Operators {
  equal = "eq",
  not_equal = "neq",
  contains = "cont",
  not_contains = "ncont",
  greater_than = "gt",
  less_than = "lt",
  before = "before",
  after = "after",
}

// define the filter value type
export type FilterValue = string | number | boolean | Date;
// define the filter conditions type: field, operators, and values
export type FilterCondition = [string, Operators, FilterValue];

// define the context state
type ContextState = {
  filters: FilterCondition[];
  setFilters(value: FilterCondition[]): void;
};

// initialize filter context
export const FilterContext = createContext<ContextState | null>(null);

// define the FilterProvider
export const FilterProvider = (props: PropsWithChildren) => {
  const [filters, setFilters] = useState<FilterCondition[]>([]);
  return (
    <FilterContext.Provider value={{ filters, setFilters }}>
      {props.children}
    </FilterContext.Provider>
  );
};
1234567891011121314151617181920212223242526272829303132333435363738

In lines 3-17, we define or build the types for the filter state. As previously mentioned above, a filter condition has three parts: field, operators and value. That's basically what we're defining here.

In lines 20-26, we define the FilterContext.

In line 30, we use the useState hook to initialize the filter state as an empty array.

Step 4: Creating the useFilters hook. #

In the same file, let's create the useFilters custom hook below:

// components/FilterProvider.tsx
import { PropsWithChildren, createContext, useContext, useState } from "react";

//...

export const useFilters = () => {
  const context = useContext(FilterContext);
  if (!context) {
    throw new Error("Please use filter provider in the parent element");
  }

  return context;
};
12345678910111213

We'll use this hook later to get direct access to the FilterContext.

Step 5: Creating the <CategoryFilter/> Component #

Next, let's create the <CategoryFilter/> component. The category component which product categories will be shown in the UI. 

First, Create the file components/CategoryFilter.tsx and copy the code below:

// components/CategoryFilter.tsx
"use client";
import products from "@/products.json";
import { Operators, useFilters } from "./FilterProvider";

// get all categories
const categories = Array.from(new Set(products.map((p) => p.category)));


const CategoryFilter = () => {
  const { filters, setFilters } = useFilters();

  const handleChange = (
    e: React.ChangeEvent<HTMLInputElement>,
    category: string
  ) => {
    if (e.target.checked) {
      // add filter condition if checked
      setFilters([...filters, ["category", Operators.equal, category]]);
    } else {
      // remove filter condition if uncheckd
      const newValue = filters.filter((condition) => condition[2] !== category);
      setFilters(newValue);
    }
  };
  return (
    <div>
      {categories.map((category, i) => {
        const checked = filters.some((condition) => {
          const [field, operator, value] = condition;
          return value === category && field === "category";
        });

        return (
          <div key={i}>
            <input
              type="checkbox"
              id={`input-${category}`}
              onChange={(e) => handleChange(e, category)}
              checked={checked}
            />
            <label htmlFor={`input-${category}`}> {category}</label>
          </div>
        );
      })}
    </div>
  );
};
export default CategoryFilter;
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849

Explanation:

First, we get all the existing categories from our products.json file. Ideally, we should pull the data from an API but for tutorial purposes, we'll make do with this.

We iterate through each of them and use <input type="checkbox"> element.

In lines 13-25, we check if the element is checked and we modify the filter state as necessary.

In lines 29-32, we use the filters state to see if a category checkbox is checked.

Next, let's modify the /app/page.tsx file to use the <CategoryFilter/> component

import styles from "./page.module.css";
import { FilterProvider } from "@/components/FilterProvider";
import CategoryFilter from "@/components/CategoryFilter";

export default function Home() {
  return (
    <FilterProvider>
      <main className={styles.main}>
        <CategoryFilter />
      </main>
    </FilterProvider>
  );
}
12345678910111213

After this step, you should have the following output or similar:

React Multiple Filters&nbsp; - CategoryFilter Component

Step 6: Creating a <ProductList/> Component #

In this step, we'll create the <ProductList/> component. The <ProductList/> component will fetch data from our API and pass the filters from our filter state.

First, create the file, components/ProductList.tsx. I added the explanations in the comments.

"use client";
import { useEffect, useState } from "react";
import { useFilters } from "./FilterProvider";
type Product = {
  productName: string;
  category: string;
  price: string;
};

const ProductList = () => {
  const [data, setData] = useState<Product[]>([]);
  const { filters } = useFilters();

  useEffect(() => {
    // 1.compose the url
    const url = new URL("http://localhost:3000/api/products");

    // 2. add a filter parameter if there are multiple filter conditions
    const filterStr = filters
      .map((condition) => {
        const [field, operation, value] = condition;
        return `${field}-${operation}-${value}`;
      })
      .join(",");
    // here's an example url with filters applied:
    // http://localhost:3000/api/products?filters=category-eq-Computers%2Ccategory-eq-Toys%2Ccategory-eq-Tools

    if (!!filters.length) {
      url.searchParams.append("filters", filterStr);
    }

    // 3. use fetch to get the data
    fetch(url)
      .then((response) => {
        if (response.ok) {
          return response.json();
        }
      })
      .then((products: Product[]) => {
        setData(products);
      });
  }, [filters]);
  return (
    <div>
      <pre>{JSON.stringify(data, null, 4)}</pre>
    </div>
  );
};
export default ProductList;
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849

In lines 19-30, we're basically transforming our filter state into a query parameter filters so that we can pass it to the backend as a GET request.

In lines 33-41, we make a simple fetch request using the URL with filters and set the state from the returned data.

In line 45, for the sake of this tutorial, we display the returned data in JSON form.

Next, let's modify the /page/app.tsx to use the <ProductsList/> component

import styles from "./page.module.css";
import { FilterProvider } from "@/components/FilterProvider";
import CategoryFilter from "@/components/CategoryFilter";
import ProductList from "@/components/ProductList";

export default function Home() {
  return (
    <FilterProvider>
      <main className={styles.main}>
        <CategoryFilter />
        <ProductList />
      </main>
    </FilterProvider>
  );
}
123456789101112131415

After this step, your output should be something like this:

React Multiple Filters - Product List

Step 7: Adding Filter Functionality to API #

Next, let's add filter functionality to our API. Since the database we use is NeDB or MongoDB, we'll transform the searchParameters from the URL to a MongoDB query. If you're using a different database, you have to transform it to the query language of that database.

Let's modify the app/api/products/route.tsx file as shown below:

import DataStore from "@seald-io/nedb";
import products from "@/products.json";
import { NextResponse } from "next/server";

// create a new in-memory database
const db = new DataStore();
// insert all products into the database
const documents = products.map((prod) => {
  return db.insertAsync(prod);
});

export async function GET(req: Request) {
  await documents;
  const url = new URL(req.url);
  const filterStr = url.searchParams.get("filters");
  console.log(filterStr);
  // transform the filter parameter into mongodb/nedb queries
  let queries = filterStr?.split(",").map((condition) => {
    const [field, operator, value] = condition.split("-");
    if (operator === "eq") {
      return { [field]: value };
    } else {
      return { [field]: { [`$${operator}`]: value } };
    }
  });

  const query = queries?.length ? { $or: queries } : {};
  const products = await db.find(query);
  return NextResponse.json(products);
}
123456789101112131415161718192021222324252627282930

In lines 18-25, we're basically transforming our filter string from this:

category-eq-Computers,category-eq-Toys,category-eq-Tools


1

To a MongoDB query like this:

{
    "$or": [
        {
            "category": "Computers"
        },
        {
            "category": "Toys"
        }
    ]
}
123456789

Transforming the filter string to a database query might require more logic if you have multiple properties like price and description.

After this step, you should be able to apply multiple filters when checking/unchecking the category checkboxes:

React Multiple Filters with API Filters Demo

That's basically it!

Full Code and Demo #

The full code can be accessed at Github: jmarioste/react-multiple-filters-tutorial.

You can access the demo on Stackblitz: React Multiple Filters Tutorial

Conclusion #

You learned how to implement multiple filters in React and use that data to query the database in the backend server. React Context API is a good way to manage the filter state between different components. The other important thing is how to represent the filter state, and how to transform it to be used by the backend server.

If you like this tutorial, please leave a like or share this article. For future tutorials like this, please subscribe to our newsletter or follow me on GitHub.

Credits: Image by Xuan Duong from Pixabay

Share this post!

Related Posts

Disclaimer

This content may contain links to products, software and services. Please assume all such links are affiliate links which may result in my earning commissions and fees.
As an Amazon Associate, I earn from qualifying purchases. This means that whenever you buy a product on Amazon from a link on our site, we receive a small percentage of its price at no extra cost to you. This helps us continue to provide valuable content and reviews to you. Thank you for your support!
Donate to ReactHustle