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.
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
And if user request is exact match from previous search
Top comments (0)