Create autocomplete search in react

Learn how to create autocomplete search functionality in react.

We will create a search functionality which will show the suggestion when something is typed such as an autocomplete feature.

Just like how when we type something type in google and it shows suggestion. We will create the UI aspect of the autocomplete not the functional part.

We will use a dummy api to fetch and show the data.

This is a little complex component because we need to handle many things. I will try to break it down and make it as simple as possible.

So grab your seat and get ready to learn something amazing 😁😎.

Search autocomplete in react.

Our component is composed of three different parts.

  • Search box which will get the input.
  • Store the state of the component and fetch the data when something is typed.
  • Show the fetched data in the suggestion display area.

We will also use some advanced techniques which will help us in performance optimization.

There are few extra packages that we will be using for our help in development.

  • 'classnames':- Using this we can merge two or more CSS classes and can also use them javascript object, we just need to name our CSS file as filename.module.css.
  • 'useDebounce':- A custom hook that will helps to debounce or delay the network call.

Following is the folder structure of our component.
React search autocomplete folder structure

Now as you got some good idea of our app, let us begin creating it.

First, import all the required packages at the top.

import React, { useState, useCallback, useRef } from "react";
import cx from "classnames";
import "./index.module.css";

const ITEMS_API_URL = "https://demo.dataverse.org/api/search";
const DEBOUNCE_DELAY = 500;

const AutoComplete = ({onChange, onSelectItem}) => {
   //code will go here
}

export default AutoComplete;

This is not a controlled component it is independent but it will return the changes to the parent. So we will be expecting two props from the parent.

One, which return the list of suggestions and second will return the item which is clicked.

Now setup the state to track the changes of the component.

  const [inputStr, setInputStr] = useState("");
  const [items, setItems] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState({status: false, message: {}});

As we need to track multiple things, we have created separate state for each.

  • inputStr will store the things we have typed in search box.
  • items will store the result from the API.
  • isLoading to track the state.
  • error to keep the error message.

Now lets create the layout of the component.

//Generate the list of returned suggestions
  const list = items.map((e, i) => (
    <span
      key={e.name + i}
      onClick={() => onItemClick(e)}
      className={"listItem"}
    >
      {e.name}
    </span>
  ));

  //Can we show the suggestions
  const canShow = list.length > 0 && !isLoading;
  return (
    <div className={"wrapper"}>
      {/* Add loading state to the search area */}
      <div
        className={cx("control", {
          ["isLoading"]: isLoading
        })}
      >
        {/* Search box */}
        <input
          type="search"
          className={"searchBox"}
          onChange={onInputChangeHandler}
        />
      </div>

      {/* Show the suggestion list*/}
      {canShow && (
        <div className={cx("displayArea", "isHoverable")}>
          {list}
        </div>
      )}
    </div>
  );

As you can see we are generating list of items from the list and displaying the result only when it is available.

Let us create the event listener for the search box which will help us to store search string.

  const onInputChangeHandler = e => {
    const { value } = e.target;

    setInputStr(value);

    delayedCallback();
  };

After saving the query string we are calling the function which will make the API call and store the data.

But If we keep making api call as user keeps typing it will really impact the performance.

That is why we will be using an optimization method call debounce of calling a function.

This will only call the function after user stops typing for a certain amount of time. This will help us to make sure that we don’t keep making unnecessary API calls.

As I have already mentioned we will be using some advance technique this what I was talking about.

For this we will have to wrap our API calling function inside the debounce. We will use the useDebounce() hook for this.

  const delayedCallback = useDebounce(fetchItems, DEBOUNCE_DELAY);

Now lets create our function to call the API.

const fetchItems = async () => {
    setIsLoading(true);

    try {
      //Update the state
      const res = await fetch(`${ITEMS_API_URL}?q=${inputStr}`);
      const json = await res.json();
      const { data } = json;
      const { items } = data;
      setItems(items);

      //Return the data to the parent
      onChange && onChange(items);
    } catch (e) {
      setError({
          status: true,
          message: e
      });
    } finally {
      setIsLoading(false);
    }
  };

Before making the API call, set the isLoading to true then if the data is fetched successfully set the items list and return the data to the parent by calling the function we have received in the prop.

If there is error then update the error message and finally irrespective of what happens update the isLoading to false.

The last thing pending is notifying the parent about which item in the list is clicked. As we have already assigned the function for the click event on the items list, lets just define it.

const onItemClick = (e) => {
    //Notify the parent
    onSelectItem && onSelectItem(e);
  } 

Complete code of search autocomplete in react.

import React, { useState, useCallback, useRef } from "react";
import cx from "classnames";
import "./index.module.css";

const useDebounce = (fn, delay, immediate = false) => {
  // ref the timer
  const timerId = useRef();

  // create a memoized debounce
  const debounce = useCallback(
    function () {
      // reference the context and args for the setTimeout function
      let context = this,
        args = arguments;

      // should the function be called now? If immediate is true
      // and not already in a timeout then the answer is: Yes
      const callNow = immediate && !timerId.current;

      // base case
      // clear the timeout to assign the new timeout to it.
      // when event is fired repeatedly then this helps to reset
      clearTimeout(timerId.current);

      // set the new timeout
      timerId.current = setTimeout(function () {
        // Inside the timeout function, clear the timeout variable
        // which will let the next execution run when in 'immediate' mode
        timerId.current = null;

        // check if the function already ran with the immediate flag
        if (!immediate) {
          // call the original function with apply
          fn.apply(context, args);
        }
      }, delay);

      // immediate mode and no wait timer? Execute the function immediately
      if (callNow) fn.apply(context, args);
    },
    [fn, delay, immediate]
  );

  return debounce;
};

const ITEMS_API_URL = "https://demo.dataverse.org/api/search";

const DEBOUNCE_DELAY = 1000;

const AutoComplete = ({onChange, onSelectItem}) => {
  const [inputStr, setInputStr] = useState("");
  const [items, setItems] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState({status: false, message: {}});

  const fetchItems = async () => {
    setIsLoading(true);

    try {
      //Update the state
      const res = await fetch(`${ITEMS_API_URL}?q=${inputStr}`);
      const json = await res.json();
      const { data } = json;
      const { items } = data;
      setItems(items);

      //Return the data to the parent
      onChange && onChange(items);
    } catch (e) {
      setError({
          status: true,
          message: e
      });
    } finally {
      setIsLoading(false);
    }
  };
  
  const delayedCallback = useDebounce(fetchItems, DEBOUNCE_DELAY);

  const onInputChangeHandler = e => {
    const { value } = e.target;

    setInputStr(value);

    delayedCallback();
  };

  const onItemClick = (e) => {
    //Notify the parent
    onSelectItem && onSelectItem(e);
  }  

  //Generate the list of returned suggestions
  const list = items.map((e, i) => (
    <span
      key={e.name + i}
      onClick={() => onItemClick(e)}
      className={"listItem"}
    >
      {e.name}
    </span>
  ));

  //Can we show the suggestions
  const canShow = list.length > 0 && !isLoading;
  return (
    <div className={"wrapper"}>
      {/* Add loading state to the search area */}
      <div
        className={cx("control", {
          ["isLoading"]: isLoading
        })}
      >
        {/* Search box */}
        <input
          type="search"
          className={"searchBox"}
          onChange={onInputChangeHandler}
        />
      </div>

      {/* Show the suggestion list*/}
      {canShow && (
        <div className={cx("displayArea", "isHoverable")}>
          {list}
        </div>
      )}
    </div>
  );
}

export default AutoComplete;

It was really amazing doing this, lets now design it.

Complete style code of our component.

We will keep the design really simple.

//index.module.css
.wrapper {
  display: inline-flex;
  position: relative;
  flex-direction: column;
}

.searchBox {
  padding: 10px;
  border: 1px solid #000;
  border-radius: 5px;
  font-size: 14px;
  color: #607d8b;
  cursor: pointer;
}

.displayArea {
  position: relative;
  left: 1px;
  top: 8px;
  max-width: 200px;
  min-height: 100px;
  background: #f5f0f0;
  box-shadow: rgba(0, 0, 0, 0.4) 0 1px 3px;
  padding: 5px;
}

.listItem {
  padding: 5px 0;
  display: block;
  border-bottom: 1px solid #37474f;
  color: #ff9800;
}

.listItem:last-child {
  border: none;
}

Input

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import AutoComplete from "./Components/Search";
import * as serviceWorker from "./serviceWorker";

ReactDOM.render(
  <div className="abc">
    <AutoComplete />
  </div>,
  document.getElementById("root")
);

Output

React Search AutoComplete