Skip to content

Making a multi-word filter in React

Posted on:September 25, 2023 at 10:02 AM

Intro

In this blog post we will build a React multi-word string filter in two steps: first a single-word filter to understand the core concept, then the multi-word filter itself.
The final result can be seen here (please forgive the lack of styling).
Its source code is available here.

Single-word filter

We will use a list of tech jobs as our model:

const jobs = [
  "frontend developer",
  "devops engineer",
  "backend dev (remote)",
  "react developer",
  "platform engineer",
  "fullstack web dev",
  "frontend dev (remote)",
  "backend developer",
];

A barebones UI for filtering such a list could consist of a heading, a text input element and an unordered list:

const jobs = [ ... ];

export function App() {

  return (
    <div>
      <h1>Single-word string filter</h1>
      <input placeholder="Filter jobs..." />
      <ul>
        {/* Filtered jobs will go here later */}
      </ul>
    </div>
  );
}

Next, we need to add the handler function for when the user types something in the input.
Does the input value need to be state? No, it doesn’t (at least in this example).
In fact, always strive to have as little state as possible, to keep your app from becoming too complex.

export function App() {

+  function onFilterChange(event: any) {
+    // Empty for now
+  }

  return (
    ...
-    <input placeholder="Filter jobs..." />
+    <input placeholder="Filter jobs..." onChange={onFilterChange} />
    ...
  );
}

Recall that React will re-render when state changes, but not when a non-state variable changes.
Thus, we need the job list as state:

+  import { useState } from 'react';

const jobs = [ ... ];

export function App() {
+  const [filteredJobs, setFilteredJobs] = useState<string[]>(jobs);
  ...
}

Before continuing, let’s stop for a moment and take a look at what we’ve written so far:

const jobs = [
  "frontend developer",
  "devops engineer",
  "backend dev (remote)",
  "react developer",
  "platform engineer",
  "fullstack web dev",
  "frontend dev (remote)",
  "backend developer",
];

export function App() {
  const [filteredJobs, setFilteredJobs] = useState<string[]>(jobs);

  function onFilterChange(event: any) {
    // Empty for now
  }

  return (
    <div>
      <h1>Single-word string filter</h1>
      <input placeholder="Filter jobs..." onChange={onFilterChange} />
      <ul>{/* Filtered jobs will go here later */}</ul>
    </div>
  );
}

Let’s now write the contents of onFilterChange(), i.e. the filtering logic:

function onFilterChange(event: any) {
  // Read the string value from input
  const eventValue: string = event.target.value;

  // Only keep jobs that contain said string
  const newFilteredJobs = jobs.filter(job =>
    job.toLowerCase().includes(eventValue.toLowerCase())
  );

  // Update state
  setFilteredJobs(newFilteredJobs);
}

The last step consists of outputting the filtered list as HTML:

export function App() {
  ...

  return (
    <div>
      <h1>Single-word string filter</h1>
      <input placeholder="Filter jobs..." onChange={onFilterChange} />
      <ul>
+        {filteredJobs.map((job) => (
+          <li key={job}>{job}</li>
+        ))}
      </ul>
    </div>
  );
}

A live demo is available here.
Source code is here.

Limitations

With the single-word filter, the user can filter an exact letter sequence like “frontend” and it will match with both “frontend developer” and “frontend developer (remote)”, but if they search for “frontend remote” there won’t be any match with the latter job.

Multi-word filter

The multi-word filter fixes the above issue with a slight increase in complexity within onFilterChange():

  function onFilterChange(event: any) {
    // Read the string value from input
-    const eventValue: string = event.target.value;
+    const eventValues: string[] = event.target.value.split(' ');

-    // Only keep jobs that contain said string
-    const newFilteredJobs = jobs.filter((job) =>
-      job.toLowerCase().includes(eventValue.toLowerCase())
-    );
+    // Only keep jobs that contain all strings obtained from .split()
+    const newFilteredJobs = jobs.filter((job) => {
+      for (const value of eventValues) {
+        if (!job.toLowerCase().includes(value.toLowerCase())) {
+          return false;
+        }
+      }
+      return true;
+    });

    // Update state
    setFilteredJobs(newFilteredJobs);
  }

What’s happening here?
Firstly, we’re splitting the user input (separator character is an empty space) into multiple words.
Secondly, the callback we pass to .filter() has been adapted to work with one or more words:

The rest of the code doesn’t need to change. Our multi-word filter is done!