tutorial // May 05, 2021

How to Implement Client-Side Search with Fuse.js

How to implement a client-side, real-time search using Fuse.js.

How to Implement Client-Side Search with Fuse.js

For some applications, running a full search server and wiring up an index is overkill. In others, it's impractical due to requirements like needing to be offline-only. While a rich search experience should by default be driven by a real search engine running on a server, in some cases, implementing client-side search is preferred.

Getting Started

To get started, for this tutorial, we're going to use the CheatCode Next.js Boilerplate as a starting point. To clone it, run:

Terminal

git clone https://github.com/cheatcode/nextjs-boilerplate.git

Next, cd into the cloned project and install its dependencies:

Terminal

cd nextjs-boilerplate && npm install

Next, let's install the fuse.js dependency via NPM:

Terminal

npm i fuse.js

Finally, let's run the project:

Terminal

npm run dev

Once all of that is complete, we're ready to get started.

Setting up our test data

First, in order to wire up our search we'll need some test data. We're going to use this list of countries from Github. Because our goal is to build this entirely client-side, we're going to create a static JavaScript file and place this content in it:

/lib/countries.js

export default [
  { code: "AF", name: "Afghanistan" },
  [...]
  { code: "ZW", name: "Zimbabwe" },
];

Next, we're ready to start building out our search. To demonstrate the setup, we're going to add a /search page in the boilerplate:

/pages/search/index.js

import React, { useState } from "react";

const Search = () => {
  const [searchQuery, setSearchQuery] = useState("");
  const [searchResults, setSearchResults] = useState([]);

  return (
    <div>
      // We'll build out our search and results UI here...
    </div>
  );
};

Search.propTypes = {};

export default Search;

To get started, here we've created a skeleton React component using the function component pattern. At the top, we define our function component with const Search. Just inside the function body, we utilize the useState() hook in React to create two state values we'll need: searchQuery and searchResults.

A few things to note when we're using the useState() hook:

  • When we call to useState() the value we pass to it represents the default value (here, for searchQuery we pass an empty string and for searchResults we pass an empty array).
  • A call to useState() returns an array containing two values: the current value and a setter to update the value (here, searchQuery is the name we use for the state value and setSearchQuery allows us to update that value).

Next, to create our base component, we return an empty <div></div> tag where the core of our search UI will go.

Initializing our index

Now, let's pull in our list of countries and create our search index using Fuse:

/pages/search/index.js

import React, { useState } from "react";
import Fuse from "fuse.js";
import countries from "../../lib/countries";

const Search = () => {
  const [searchQuery, setSearchQuery] = useState("");
  const [searchResults, setSearchResults] = useState([]);

  const searchIndex = new Fuse(countries, {
    includeScore: true,
    threshold: 0.4,
    keys: ["name"],
  });

  return (
    <div>
      // We'll build out our search and results UI here...
    </div>
  );
};

Search.propTypes = {};

export default Search;

We've added a few things here. First, up at the top, we import the countries.js file that we created earlier. Next, we create a new variable searchIndex which is set to new Fuse() passing it two things: our list of countries (the data we want to add to the index) and an options object with three settings:

  1. includeScore tells Fuse that we want each search result to receive a relevancy score and we want that score returned in the search results data.
  2. threshold is a number which dictates how "fuzzy" our search should be. A threshold of 0 means the search has to match exactly while a threshold of 1.0 means anything will match. 0.4 is arbitrary here, so feel free to play with it.
  3. keys is an array of strings describing the object keys we want to search. In this case, we only want our search to be against the name property on each of our country objects.

Though it may not look like much, this is the core of working with Fuse. Simple, right? With this, now we're ready to set up a search UI and see some real-time results.

Wiring up the search UI

First, we need to add an <input /> where a user can type in a search query:

/pages/search/index.js

import React, { useState } from "react";
import Fuse from "fuse.js";
import countries from "../../lib/countries";

const Search = () => {
  const [searchQuery, setSearchQuery] = useState("");
  const [searchResults, setSearchResults] = useState([]);

  const searchIndex = new Fuse(countries, {
    includeScore: true,
    threshold: 0.4,
    keys: ["name"],
  });

  const handleSearch = (searchQuery) => {
    setSearchQuery(searchQuery);
    const results = searchIndex.search(searchQuery);
    setSearchResults(results);
  };

  return (
    <div>
      <div className="mb-4">
        <input
          type="search"
          name="search"
          className="form-control"
          value={searchQuery}
          onChange={(event) => handleSearch(event.target.value)}
        />
      </div>
    </div>
  );
};

Search.propTypes = {};

export default Search;

We're adding two big things here: first, down in the return value (our component's markup), we've added an <input /> tag with a type of search (this toggles the browser's special features for a search input like a clear button).

We've also given it a className of form-control to give it some base styling via Bootstrap (included in the boilerplate we're using). Next, we set the input's value to our searchQuery state value and then add an onChange handler, passing a function which calls to another function we've defined up above, handleSearch(), passing the event.target.value which represents the current value typed into the search input.

/pages/search/index.js

const handleSearch = (searchQuery) => {    
  setSearchQuery(searchQuery);
  const results = searchIndex.search(searchQuery);
  setSearchResults(results);
};

Zooming in on that handleSearch() function, this is where the magic happens. First, we make sure to set our searchQuery (event.target.value, passed into the handleSearch function as searchQuery) so that our UI updates as the user types. Second, we perform our actual search, using the .search() method returned as part of the Fuse index instance (what we store in the searchIndex variable).

Finally, we take the results we get back from Fuse and then set those on to state. Now, we're ready to render our results and see this whole thing work in real-time.

Wiring up the results UI

To finish up, next, we need to render out our search results. Remember that earlier as part of the options object we passed to Fuse, we added an includeScore setting, set to true. Before we render our search results, we want to create a sorted version of the results, based on this score value.

/pages/search/index.js

import React, { useState } from "react";
import Fuse from "fuse.js";
import countries from "../../lib/countries";

const Search = () => {
  const [searchQuery, setSearchQuery] = useState("");
  const [searchResults, setSearchResults] = useState([]);
  const sortedSearchResults = searchResults.sort((resultA, resultB) => {
    return resultA.score - resultB.score;
  });

  const searchIndex = new Fuse(countries, {
    includeScore: true,
    threshold: 0.4,
    keys: ["name"],
  });

  const handleSearch = (searchQuery) => {
    setSearchQuery(searchQuery);
    const results = searchIndex.search(searchQuery);
    setSearchResults(results);
  };

  return (
    <div>
      <div className="mb-4">
        <input
          type="search"
          name="search"
          className="form-control"
          value={searchQuery}
          onChange={(event) => handleSearch(event.target.value)}
        />
      </div>
    </div>
  );
};

Search.propTypes = {};

export default Search;

Here, we've added a sortedSearchResults variable just benath our useState() declaration for the searchResults variable. Assigned to it is the result of calling searchResults.sort() (the native JavaScript Array .sort() method). To it, we pass a comparison function which takes in two arguments: the current item we're comparing resultA (the one being iterated over in the sort) and the next item after it resultB.

Our comparison is to check the difference between each score. Automatically, the .sort() method will use this to give us back a sorted copy of our search results array, by each result's score property.

Now we're ready to render the results. Let's add some boilerplate code and then walk through it:

/pages/search/index.js

import React, { useState } from "react";
import Fuse from "fuse.js";
import countries from "../../lib/countries";

const Search = () => {
  const [searchQuery, setSearchQuery] = useState("");
  const [searchResults, setSearchResults] = useState([]);
  const sortedSearchResults = searchResults.sort((resultA, resultB) => {
    return resultA.score - resultB.score;
  });

  const searchIndex = new Fuse(countries, {
    includeScore: true,
    threshold: 0.4,
    keys: ["name"],
  });

  const handleSearch = (searchQuery) => {
    setSearchQuery(searchQuery);
    const results = searchIndex.search(searchQuery);
    setSearchResults(results);
  };

  return (
    <div>
      <div className="mb-4">
        <input
          type="search"
          name="search"
          className="form-control"
          value={searchQuery}
          onChange={(event) => handleSearch(event.target.value)}
        />
      </div>
      {sortedSearchResults.length > 0 && (
        <ul className="list-group">
          {sortedSearchResults.map(({ item }) => {
            return (
              <li className="list-group-item" key={item.name}>
                {item.name} ({item.code})
              </li>
            );
          })}
        </ul>
      )}
    </div>
  );
};

Search.propTypes = {};

export default Search;

This finishes out our search UI. Here, we've taken the sortedSearchResults we created and first check to see if it has a length greater than 0. If it does, we want to render our search results <ul></ul>. If not, we want it to hide. For that list, we've used the Bootstrap list-group to give our search results some style along with the list-group-item class on each of our individual search results.

For each search result, we just render the name and code (in parentheses) side-by-side.

DfVEUr6TPLzZURzO/ntfPNzy61eoZ9sLb.0
Searching our countries list via Fuse.

That's it! Now, if we load up our app in the browser and head to http://localhost:5000/search, we should see our working search UI.

Wrapping up

In this tutorial, we learned how to build a client-side, real-time search using Fuse. We learned how to set up a simple search component in React, create a search index with Fuse (populating it with data in the process), and performing a search query against that index.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode