RK

Rem Kim

Rem Kim - 2021-08-20

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

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

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

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

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

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> ); };

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

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; `;

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> ); };

Fuse.js

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

touch Autocomplete/useFuse.js

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; }

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> ); };

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); };

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); };

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>

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

react.js
fuse.js
styled-components
guide
autocomplete

© Rem Kim - Powered by Next.js