DEV Community

Rem Kim
Rem Kim

Posted on

How to make Autocomplete in React.js with Fuse.js

React Autocomplete guide
In this guide I will teach you how to build Autocomplete input with Fuse.js and React.js

Demo project setup

First up let's create new React app. This should setup React using create-react-app.

npx create-react-app autocomplete-demo
cd autocomplete-demo
yarn start
Enter fullscreen mode Exit fullscreen mode

Next we will need 2 extra dependencies Fuse.js and Styled Components.

Fuse.js will help us with fuzzy search on client side since we will not be storing user searches anywhere but on a client side.

Styled Components is to make everything look pretty

Let's install those

yarn add fuse.js styled-components
Enter fullscreen mode Exit fullscreen mode

Now that everything is installed, let's get to coding part!

Autocomplete component

First create folder named Autocomplete and index.js file

mkdir Autocomplete
touch Autocomplete/index.js
Enter fullscreen mode Exit fullscreen mode

There are 3 core elements in this Autocomplete component implementation:

  • Input for entering text
  • Suggestions list
  • Shadow word match

export const Autocomplete = () => {
  const [searchTerm, setText] = useState("");
  const [searchHistory, setHistory] = useState([]);
  const handleSubmit = (ev) => {
    ev.preventDefault();
    const set = new Set([...searchHistory, searchTerm]);
    setHistory([...set]);
    setText("");
  };
  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input
          type="search" // this gives us ability to clear input with Esc key
          value={searchTerm}
          onChange={(ev) => setText(ev.target.value)}
          placeholder="eg. I do autocomplete for living"
        />
      </form>
      {/* suggestions list */}
      <div>
        <div show={searchTerm.length > 0 && searchHistory.length > 0}>
          <ol>
            {searchHistory.map((search) => (
              <li key={search}>{search}</li>
            ))}
          </ol>
        </div>
      </div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

This is what we have so far. Every time user submits search query we add it to searchHistory and show it in the list.
step1

Now I know this is already looks very pretty but let's do some styling. Let's create styles.js file and add our styled-components there.

touch Autocomplete/styles.js
Enter fullscreen mode Exit fullscreen mode

styles.js

import styled from "styled-components";

export const AutocompleteContainer = styled.div`
  width: 450px;
  margin: 0 auto;
`;

export const SuggestionsContainer = styled.div`
  position: relative;
`;

export const SuggestionsDropdown = styled.div`
  position: absolute;
  width: 100%;
  border: 2px solid gainsboro;
  border-radius: 4px;
  margin-top: 2px;
  box-sizing: border-box;
  display: ${({ show }) => (show ? "block" : "none")};
`;

export const Input = styled.input`
  width: 100%;
  padding: 1.1rem;
  border: 2px solid gainsboro;
  border-radius: 4px;
  font-size: 1.2rem;
  z-index: 10;
  background: transparent;

  &:focus {
    outline: none;
    border-color: lightblue;
    box-shadow: 0 0 4px lightblue;
  }
`;

export const List = styled.ol`
  list-style: none;
  text-align: start;
  font-size: 1.1rem;
  padding: 0;
  margin: 0;
`;

export const SuggestionItem = styled.li`
  padding: 1.1rem;
  transition: all 250ms ease-in-out;
  &:hover {
    background: #cccccc;
  }
`;

export const MatchShadow = styled.div`
  position: absolute;
  border: 2px solid transparent;
  padding: 1.1rem;
  border-radius: 4px;
  font-size: 1.2rem;
  color: #cccccc;
  z-index: -1;
  user-select: none;
  background: transparent;
  top: 0;
`;
Enter fullscreen mode Exit fullscreen mode

This should be enough, this contains enough styling for every element that we use.

Autocomplete

import { useState } from "react";
import {
  AutocompleteContainer,
  Input,
  List,
  SuggestionItem,
  SuggestionsContainer,
  SuggestionsDropdown
} from "./styles";

export const Autocomplete = () => {
  const [searchTerm, setText] = useState("");
  const [searchHistory, setHistory] = useState([]);
  const handleSubmit = (ev) => {
    ev.preventDefault();
    const set = new Set([...searchHistory, searchTerm]);
    setHistory([...set]);
    setText("");
  };
  return (
    <AutocompleteContainer>
      <form onSubmit={handleSubmit} style={{ position: "relative" }}>
        <Input
          type="search"
          value={searchTerm}
          onChange={(ev) => setText(ev.target.value)}
          placeholder="eg. I do autocomplete for living"
        />
      </form>
      {/* suggestions list */}
      <SuggestionsContainer>
        <SuggestionsDropdown
          show={searchTerm.length > 0 && searchHistory.length > 0}
        >
          <List>
            {searchHistory.map((search) => (
              <SuggestionItem key={search}>{search}</SuggestionItem>
            ))}
          </List>
        </SuggestionsDropdown>
      </SuggestionsContainer>
    </AutocompleteContainer>
  );
};
Enter fullscreen mode Exit fullscreen mode

Fuse.js

Time to add fuse.js and make our Autocomplete somewhat smart in its suggestions.

touch Autocomplete/useFuse.js
Enter fullscreen mode Exit fullscreen mode

Here is a useFuse hook that we will use to make suggestions.

import { useEffect, useRef, useState } from "react";
import Fuse from "fuse.js";

export function useFuse(searchTerm, items, options = {}) {
  const fuse = useRef();
  const [suggestions, setSuggestions] = useState([]);
  useEffect(() => {
    fuse.current = new Fuse(items, options);
  }, [items, options]);
  useEffect(() => {
    const items = fuse.current.search(searchTerm);
    setSuggestions(items.map(({ item }) => item));
  }, [searchTerm]);

  return suggestions;
}
Enter fullscreen mode Exit fullscreen mode

Every time we update searchTerm fuse will run search on that updated term and set new suggestions based on it.

Injecting useFuse into Autocomplete component

import { useState } from "react";
import {
  AutocompleteContainer,
  Input,
  List,
  MatchShadow,
  SuggestionItem,
  SuggestionsContainer,
  SuggestionsDropdown
} from "./styles";
+import { useFuse } from "./useFuse";

export const Autocomplete = () => {
  const [searchTerm, setText] = useState("");
  const [searchHistory, setHistory] = useState([]);
  const handleSubmit = (ev) => {
    ev.preventDefault();
    const set = new Set([...searchHistory, searchTerm]);
    setHistory([...set]);
    setText("");
  };
+  const suggestions = useFuse(searchTerm, searchHistory);
+  const exactMatch = (query, text) => {
+    const regex = new RegExp(`^${query}`);
+    return regex.test(text);
+  };
  return (
    <AutocompleteContainer>
      <form onSubmit={handleSubmit} style={{ position: "relative" }}>
        <Input
          type="search"
          value={searchTerm}
          onChange={(ev) => setText(ev.target.value)}
          placeholder="eg. Mazda, Toyota, Porshe"
        />
+        <MatchShadow>
+          {suggestions.length > 0 &&
+            exactMatch(searchTerm, suggestions[0]) &&
+            suggestions[0]}
+        </MatchShadow>
      </form>
      {/* suggestions list */}
      <SuggestionsContainer>
        <SuggestionsDropdown
          show={searchTerm.length > 0 && suggestions.length > 0}
        >
          <List>
            {suggestions.map((search) => (
              <SuggestionItem key={search}>{search}</SuggestionItem>
            ))}
          </List>
        </SuggestionsDropdown>
      </SuggestionsContainer>
    </AutocompleteContainer>
  );
};

Enter fullscreen mode Exit fullscreen mode

This block adds usage of useFuse and pipes in searchHistory and searchTerm.

  const suggestions = useFuse(searchTerm, searchHistory);
  const exactMatch = (query, text) => {
    const regex = new RegExp(`^${query}`);
    return regex.test(text);
  };
Enter fullscreen mode Exit fullscreen mode

This is a helper function that will check if suggestion is exact match with query that user types in. If yes we will show autocomplete shadow of the suggested word in the input. Giving it a very nice touch for UX.

  const exactMatch = (query, text) => {
    const regex = new RegExp(`^${query}`);
    return regex.test(text);
  };
Enter fullscreen mode Exit fullscreen mode

Finally here we add MatchShadow styled component and adding our exact match and other conditional checks to make sure we show it only when we have suggestions and it is an exact match.

        <MatchShadow>
          {suggestions.length > 0 &&
            exactMatch(searchTerm, suggestions[0]) &&
            suggestions[0]}
        </MatchShadow>
Enter fullscreen mode Exit fullscreen mode

Result

With all that in place let's check final result!

After user types in few searches and submits them
Populate history

And if user request is exact match from previous search
exact match search

I hope you found this guide useful! Thank you for reading.

Links

Latest comments (0)