paint-brush
Autocomplete Search Component With React and TypeScriptby@ljaviertovar
6,699 reads
6,699 reads

Autocomplete Search Component With React and TypeScript

by L Javier TovarNovember 28th, 2022
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

In this tutorial, we will build a simple search component that offers users suggestions about what they are typing without third-party libraries.

Company Mentioned

Mention Thumbnail
featured image - Autocomplete Search Component With React and TypeScript
L Javier Tovar HackerNoon profile picture

How to show suggestions of data from an API Rest

Nowadays, one of the most widely used components of a website is the search engines with autocomplete or suggestions.


It is usually the first component with which the user interacts since it is more practical to perform a search and go directly to what we need. These components are essential in sites such as e-commerce for a good user experience.

In this tutorial, we will build a simple search component that offers users suggestions about what they are typing without third-party libraries.

What is Autocomplete Search?

Autocomplete is a pattern used to display query suggestions.


An autocomplete search, also called “predictive search” or “autosuggest,” is a component that the user types in the input field that will suggest various predictions or possible results of how the search might be completed.


Autocomplete works with a search engine that learns and improves the suggested results as it is fed by the searches its users perform.


In this case, we will not see more about search engines because it is out of the scope of the tutorial. If you want to learn more about this topic, you can look at this site. Without further ado, let’s get to programming.


Setting Up Autocomplete search

We create our application with vite with the following command:


yarn create vite autocomplete-search --template react-ts


We install the dependencies that we will need in the project:


yarn add @nextui-org/react


In this case, I am only going to use a third-party library for the styles you can use whatever you want:


  • nextui a Javascript/CSS framework


After that, we create the following folder structure for the project:


src/
├── components/
│   └── ui/
│       ├── Autocomplete.tsx
│       ├── Autocomplete.tsx
│       ├── index.ts
│       └── ui.module.css
├── hooks/
│   └── useAutocomplete.ts
├── ts/
│   └──interfaces/
│      └── Country.interface.ts
├── App.tsx
└── main.tsx


Components

AutocompleteWrapper.tsx This component is only used as a container or wrapper of Autocomplete.tsx and is where we are going to request the data we need.


We use the restcountries API for the tutorial and only use English-speaking countries so that the query will be as follows:


https://restcountries.com/v3.1/lang/eng


Autocomplete.tsx This is the main component, and it has two sections. The first section is the input element, and the second is the list of suggestions.


Usually, a <ul>or <ol>element is used, but in this tutorial, we will use Rows components inside a Next UI Card component.



import { Card, Col, Input, Row, Text, User } from "@nextui-org/react"
import { useEffect, useRef, useState } from "react"

import { Country } from "../../ts/interfaces/Country.interface"

import classes from "./ui.module.css"

interface Props {
  data: Country[];
}

const Autocomplete = ({ data }: Props) => {
  const inputSearchRef = useRef < HTMLInputElement > null

  const [searchedValue, setSearchedValue] = useState("")
  const [suggestions, setSuggestions] = useState < Country[]> []
  const [selectedSuggestion, setSelectedSuggestion] = useState("")
  const [activeSuggestion, setActiveSuggestion] = useState(0)

  useEffect(() => {
    if (inputSearchRef.current) {
      inputSearchRef.current.focus()
    }
  }, [])

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
    if (event.target.value !== "") {
      const filteredSuggestions = data.filter((itemData) => {
        const value = event.target.value.toUpperCase()
        const name = itemData.name.common.toUpperCase()

        return value && name.startsWith(value) && name !== value
      })
      setSearchedValue(event.target.value)
      setSuggestions(filteredSuggestions)
    }
  }

  const handleKeyDown = (
    event: React.KeyboardEvent<HTMLInputElement>
  ): void => {
    if (event.key === "ArrowDown" && activeSuggestion < suggestions.length) {
      setActiveSuggestion(activeSuggestion + 1)
    } else if (event.key === "ArrowUp" && activeSuggestion > 1) {
      setActiveSuggestion(activeSuggestion - 1)
    } else if (event.key === "Enter") {
      setSearchedValue(suggestions[activeSuggestion - 1].name.common)
      setSelectedSuggestion(suggestions[activeSuggestion - 1].name.common)
      setSuggestions([])
      setActiveSuggestion(0)
    }
  }

  const handleClick = (value: string) => {
    setSelectedSuggestion(value)
    setSearchedValue(value)
    setSuggestions([])
    setActiveSuggestion(0)
    //do something else
  }

  return (
    <div className={classes.autocomplete}>
      <Input
        bordered
        labelPlaceholder="Search your Country"
        size="xl"
        value={searchedValue}
        onChange={handleChange}
        onKeyDown={handleKeyDown}
        ref={inputSearchRef}
        color="secondary"
      />

      <Card css={{ marginTop: "0.5rem" }}>
        <Card.Body css={{ padding: "0" }}>
          {!suggestions.length &&
          searchedValue.length &&
          !selectedSuggestion.length ? (
            <Row className={classes.itemListNot}>
              <Col>
                <Text>Nothing to show :(</Text>
              </Col>
            </Row>
          ) : (
            <>
              {suggestions.map(({ name, flags }: Country, index) => (
                <Row
                  key={index}
                  className={`${classes.itemList} ${
                    index === activeSuggestion - 1 ? classes.activeItem : ""
                  }`}
                  onClick={() => handleClick(name.common)}
                >
                  <Col>
                    <User src={flags.svg} name={name.common} squared />
                  </Col>
                </Row>
              ))}
            </>
          )}
        </Card.Body>
      </Card>

      <Text size="$xs">Country selected: {selectedSuggestion}</Text>
    </div>
  )
}

export default Autocomplete


First, we create the types we need. The API returns us a large amount of data that we will not use, so simplifying the information and the types would look like this:


export type Country = {
  name: Name
  flags: Flags
}
type Name = {
  common: string
}
type Flags = {
  png: string
  svg: string
}


After that, we will create the following states:


searchedValue — Here, we will store the text user is typing.


suggestions—Here, we will store the suggestions that match what the user writes.


selectedSuggestion — Here, we will store the option selected by the user.


activeSuggestion — Here, we will store the index of the suggestions shown. We will use it to know which suggestion is selected by the keyboard.


Now, we need to create the functions that will react to the events of the input element and the results list.


handleChange() This function will be executed every time the user types something in the input element. We’ll validate if what is entered isn’t an empty value. Otherwise, we’ll set the states to their initial values.


If the value received in the input element isn’t empty, the function will be executed and display the suggestions that match the value entered.


handleClick() This function will be executed when the user selects a suggestion; we save the selected value and set the remaining states to their initial values.


handleKeyDown() This function will be executed when an event is detected on the keyboard, so you can browse through the suggestions and select one.


Finally, we added a useEffect to focus on the input element when the component is mounted.


That’s all! We already have an autocomplete search we can use in any project by passing the input reference and the data to the filter.


As an additional step and good practice, we will take the functionality to a custom hook, and our component will be cleaner and more readable.


import { useEffect, useState } from "react";

import { Country } from "../ts/interfaces/Country.interface";

const useAutocomplete = (
  data: Country[],
  inputSearchRef: HTMLInputElement | null
) => {
  const [searchedValue, setSearchedValue] = useState("");
  const [suggestions, setSuggestions] = useState < Country[] > [];
  const [selectedSuggestion, setSelectedSuggestion] = useState("");
  const [activeSuggestion, setActiveSuggestion] = useState(0);

  useEffect(() => {
    if (inputSearchRef) {
      inputSearchRef.focus();
    }
  }, []);

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
    if (event.target.value !== "") {
      const filteredSuggestions = data.filter((itemData) => {
        const value = event.target.value.toUpperCase();
        const name = itemData.name.common.toUpperCase();

        return value && name.startsWith(value) && name !== value;
      });
      setSearchedValue(event.target.value);
      setSuggestions(filteredSuggestions);
    } else {
      setSearchedValue("");
      setSuggestions([]);
      setSelectedSuggestion("");
      setActiveSuggestion(0);
    }
  };

  const handleKeyDown = (
    event: React.KeyboardEvent<HTMLInputElement>
  ): void => {
    if (event.key === "ArrowDown" && activeSuggestion < suggestions.length) {
      setActiveSuggestion(activeSuggestion + 1);
    } else if (event.key === "ArrowUp" && activeSuggestion > 1) {
      setActiveSuggestion(activeSuggestion - 1);
    } else if (event.key === "Enter") {
      setSearchedValue(suggestions[activeSuggestion - 1].name.common);
      setSelectedSuggestion(suggestions[activeSuggestion - 1].name.common);
      setSuggestions([]);
      setActiveSuggestion(0);
    }
  };

  const handleClick = (value: string) => {
    setSearchedValue(value);
    setSuggestions([]);
    setSelectedSuggestion(value);
    setActiveSuggestion(0);
    //do something else
  };

  return {
    searchedValue,
    suggestions,
    selectedSuggestion,
    activeSuggestion,
    handleChange,
    handleKeyDown,
    handleClick,
  };
};

export default useAutocomplete;


import { useEffect, useRef } from "react";
import { Card, Col, Input, Row, Text, User } from "@nextui-org/react";

import { Country } from "../../ts/interfaces/Country.interface";

import useAutocomplete from "../../hooks/useAutocomplete";

import classes from "./ui.module.css";

interface Props {
  data: Country[];
}

const Autocomplete = ({ data }: Props) => {
  const inputSearchRef = useRef < HTMLInputElement > null;

  useEffect(() => {
    if (inputSearchRef.current) {
      inputSearchRef.current.focus();
    }
  }, []);

  const {
    searchedValue,
    suggestions,
    selectedSuggestion,
    activeSuggestion,
    handleChange,
    handleKeyDown,
    handleClick,
  } = useAutocomplete(data, inputSearchRef.current);

  return (
    <div className={classes.autocomplete}>
      <Input
        bordered
        labelPlaceholder="Search your Country"
        size="xl"
        value={searchedValue}
        onChange={handleChange}
        onKeyDown={handleKeyDown}
        ref={inputSearchRef}
        color="secondary"
      />

      <Card css={{ marginTop: "0.5rem" }}>
        <Card.Body css={{ padding: "0" }}>
          {!suggestions.length &&
          searchedValue.length &&
          !selectedSuggestion.length ? (
            <Row className={classes.itemListNot}>
              <Col>
                <Text>Nothing to show :(</Text>
              </Col>
            </Row>
          ) : (
            <>
              {suggestions.map(({ name, flags }: Country, index) => (
                <Row
                  key={index}
                  className={`${classes.itemList} ${
                    index === activeSuggestion - 1 ? classes.activeItem : ""
                  }`}
                  onClick={() => handleClick(name.common)}
                >
                  <Col>
                    <User src={flags.svg} name={name.common} squared />
                  </Col>
                </Row>
              ))}
            </>
          )}
        </Card.Body>
      </Card>

      <Text size="$xs">Country selected: {selectedSuggestion}</Text>
    </div>
  );
};

export default Autocomplete;


The app looks like this:


Autocomplete search

See the demo here.


Repo here.

Conclusion

We have created a simple search component by applying filters to the data received this search could get more and more complicated depending on the case. After capturing the selected value, you could add more functionality, such as displaying the details of the country selected.

I hope this tutorial has been useful for you and that you have learned new things in developing this application.


Originally published here.