Let's Build an Address Autocomplete in ReactJS
Tags: react, tutorial, typescript, webdev
Published: 2020-01-14 09:16:07

Times to times we need to ask the user to provide us with an address.
It might be because we are gathering shipping information, for example. 📦

Anyway, we are in need of accurate results but, guess what?

We do not trust our users, of course. 😂

So, how do you obtain an accurate address out of a distracted user which doesn't really pay attention? 🤔

One solution is to use an address autocomplete so that a partial address like `via cervantes 55` would be expanded to its correct form `Italia, Napoli, Via Miguel Cervantes De Saavedra, 55` without the user writing it all down by himself. 😎

A good example of an address autocomplete is [Algolia Places.](https://community.algolia.com/places/)

![Algolia Places](https://i.ibb.co/L07MNSS/community-algolia-com-places.png)

Actually, we will come up with something that resembles it!

As a matter of fact, I'm going to show you how to build such a component with `ReactJS` and the help of [`use-here-api`](https://github.com/CloudPower97/use-here-api), a library that exposes a collection of convenient hooks that let you easily integrate HERE RESTful API services in your projects.

If you want to learn more about `use-here-api`, please, take a look at [this post](https://dev.to/cloudpower97/a-collection-of-useful-react-hooks-for-geocoding-purposes-and-more-l1p). 🤓

{% link https://dev.to/cloudpower97/a-collection-of-useful-react-hooks-for-geocoding-purposes-and-more-l1p %}

Without further due, let's get our hands dirty!

## Let's set up the project 👨‍💻

First of all, we should create a basic React project.

To accomplish this task, simply open up a terminal and type in the following:

```bash
yarn create react-app address-autocomplete --template typescript
```

*Note that it may take a couple of minutes for this command to complete.*

We will be using `Sass`, so we need to install `node-sass` as well:

```bash
yarn add node-sass
```

We are now going to delete a couple of unneeded files, namely:
- logo.svg
- App.css
- App.test.tsx

Now, before actually writing some real code, we are going to create a new folder `components` inside `src` and, in that folder, create another one called `AddressAutocomplete`.

Inside that folder we are going to create a couple of more files:

- package.json

```json
{
  "main": "AddressAutocomplete.tsx"
}
```

- AddressAutocomplete.tsx

```tsx
import React, { FC } from 'react'
import Styles from './AddressAutocomplete.module.scss';

const AddressAutocomplete: FC = () => <input placeholder="Enter an address" />

export default AddressAutocomplete
```

- AddressAutocomplete.module.scss

Finally, let's open up `App.tsx` and modify it like so:

```tsx
import React, { FC } from 'react';
import AddressAutocomplete from './components/AddressAutocomplete';

const App: FC = () => (
  <AddressAutocomplete
    onAutocomplete={(address: any) => {
      console.log(address);
    }}
  />
);

export default App;
```

You should come up with a folder structure just like this image below:

![Correct folder structure for this project](https://i.ibb.co/Yy3NNbz/Screenshot-from-2020-01-13-20-52-10.png)

## Ready to play with `use-here-api` 🗺️

It's now time to

```bash
yarn add @cloudpower97/use-here-api
```

Create a `.env` file at the root of your project and type in the following:

```env
REACT_APP_HERE_APP_CODE="..."
REACT_APP_HERE_APP_ID="..."
```

Obviously, make sure to actually replace `"..."` with the correct pieces of information. 👀

We are now going to authenticate our requests towards HERE API with the help of `configureAutentication`.

```tsx
import {
  configureAuthentication,
  useAutocomplete
} from '@cloudpower97/use-here-api';

const {
  REACT_APP_HERE_APP_ID: app_id,
  REACT_APP_HERE_APP_CODE: app_code
} = process.env;

if (app_code && app_id) {
  configureAuthentication({
    app_code,
    app_id
  });
}
```

From now on, every request we make with any of the hooks exposed by `use-here-api` is going to use the provided credentials. 🔐

To learn more about credentials and authentication methods, you can [read this](https://github.com/CloudPower97/use-here-api#usage).

### Creating the interface 🧱

We should now create an interface to define `AddressAutocomplete` `props`

```tsx
interface AddressAutocompleteProps extends HTMLAttributes<HTMLInputElement> {
  value?: string;
  onAutocomplete?: Function;
}
```

`onAutocomplete` is an optional callback that is going to be invoked, if provided, once the user selects one of the results.

As we are going to spread every other `prop` in an `input` element, we `extends HTMLAttributes<HTMLInputElement>`.

### Implementing the component 🛠️

Now, as `use-here-api` is a typesafe wrapper around `HERE API`, we can also have a look at the official documentation, that reads:

> The HERE Geocoder Autocomplete API is a REST API that you can integrate into web applications to help users obtain better results for address searches with fewer keystrokes. Spatial and region filters can be used to return suggestions with greater relevance to end-users, such as results that are within a specified country or in the proximity of the current location.

> The Geocoder Autocomplete API retrieves a complete address and an ID. You can subsequently use the Geocoder API to geocode the address based on the ID and thus obtain the geographic coordinates of the address.

> -- <cite>[Geocoder Autocomplete API](https://developer.here.com/documentation/geocoder-autocomplete/dev_guide/topics/what-is.html)</cite>


And, if we head over to [API Reference](https://developer.here.com/documentation/geocoder-autocomplete/dev_guide/topics/resource-suggest.html) we will see that the only required parameter is `query`, the actual search text.

For this component, we are going to use `beginHighlight` and `endHighlight` as well, in order to highlight the matched characters.

The wrapper around this API is exposed through [`useAutocomplete`](https://github.com/CloudPower97/use-here-api/blob/master/docs/useAutocomplete.md) hook. 🎣

At this point, we just need to make sure that we make use of this hook with the `query` params fed with user input.

```tsx
const AddressAutocomplete: FC<AddressAutocompleteProps> = ({
  value = '',
  placeholder = 'Enter an address',
  onAutocomplete,
  ...props
}) => {
  const [{ data }, fetchSuggestions] = useAutocomplete();
  const [location, setLocation] = useState<string>(value);
  const [isMenuOpen, setMenuOpen] = useState<boolean>(false);

  useEffect(() => {
    if (data) {
      setMenuOpen(true);
    }
  }, [data]);

  return (
    <>
      <div className={Styles.SuggestionWrapper}>
        <input
          placeholder={placeholder}
          onChange={({ currentTarget: { value } }) => {
            setLocation(value);

            if (value.length >= 3) {
              fetchSuggestions({
                query: value,
                beginHighlight: '<b>',
                endHighlight: '</b>'
              });
            }
          }}
          className={Styles.SuggestionInput}
          value={location}
          tabIndex={0}
          {...props}
        />
        {data&& isMenuOpen && (
          <ul className={Styles.SuggestionsList}>
            {data?.suggestions?.length === 0 && (
              <li className={Styles.SuggestionItem}>No results found...</li>
            )}
            {data?.suggestions?.map(suggestion => (
              <li
                key={suggestion.locationId}
                className={Styles.SuggestionItem}
                onClick={() => {
                  setLocation(suggestion.label.replace(/<[^>]+>/g, ''));
                }}
                tabIndex={1}
              >
                <span
                  className={Styles.SuggestionLabel}
                  dangerouslySetInnerHTML={{ __html: suggestion.label }}
                />{' '}
                <span
                  className={Styles.AdditionalSuggestion}
                  dangerouslySetInnerHTML={{
                    __html: `${suggestion.address.county}, ${suggestion.address.state}, ${suggestion.address.country}`
                  }}
                />
              </li>
            ))}
          </ul>
        )}
      </div>
    </>
  );
};

```

### Retrieve Geographic Coordinates 📍

The component is getting shape!

The user can now type in a partial address and then select the correct one in the dropdown filled with results retrieved from the HERE API! 

However, we should now retrieve geographic coordinates for the selected address as well. 🌍

As a matter of fact, as also noted in the [guide section](https://developer.here.com/documentation/geocoder-autocomplete/dev_guide/topics/example-location-id.html#example-location-id__request-location-id),

> after the user has selected a suggestion, you can retrieve the location details such as the geographic coordinates via the HERE Geocoder API, using look-up by locationId.

In order to accomplish this task, we are going to use another hook exposed by `use-here-api`, namely `useForwardGeocoding`.

```tsx
const [{ data: geocodeData }, fetchLocation] = useForwardGeocoding();


useEffect(() => {
    if (geocodeData) {
        onAutocomplete && 
   onAutocomplete(geocodeData.response.view[0].result[0]);
    }
}, [geocodeData, onAutocomplete]);

```

We just need to call `fetchLocation` when clicking on a result, providing the `locationid` of the latter.

```tsx
<li
  key={suggestion.locationId}
  className={Styles.SuggestionItem}
  onClick={() => {
    setLocation(suggestion.label.replace(/<[^>]+>/g, ''));
    setMenuOpen(false);
    fetchLocation({
      locationid: suggestion.locationId,
      jsonattributes: 1
    });
  }}
  tabIndex={1}
>
  <span
    className={Styles.SuggestionLabel}
    dangerouslySetInnerHTML={{ __html: suggestion.label }}
  />{' '}
  <span
    className={Styles.AdditionalSuggestion}
    dangerouslySetInnerHTML={{
      __html: `${suggestion.address.county}, ${suggestion.address.state}, ${suggestion.address.country}`
    }}
  />
</li>
```

### Add the needed styles 💅

We are now going to add some styles to our component.

Let's get straight to `AddressAutocomplete.module.scss`:

```scss
.SuggestionWrapper {
  position: relative;
  width: 100%;
}

.SuggestionInput {
  line-height: 1.15;
  overflow: visible;
  background-color: transparent;
  border: none;
  border-bottom: 1px solid #9e9e9e;
  border-radius: 0;
  outline: none;
  height: 3rem;
  width: 100%;
  font-size: 16px;
  margin: 0 0 8px 0;
  padding: 0;
  box-shadow: none;
  box-sizing: content-box;
  transition: box-shadow .3s, border .3s;
}

.SuggestionsList {
  background-color: white;
  box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14),
    0 3px 1px -2px rgba(0, 0, 0, 0.12), 0 1px 5px 0 rgba(0, 0, 0, 0.2);
  left: 0;
  list-style-type: none;
  margin: 0px;
  max-height: 225px;
  overflow: auto;
  padding: 0;
  position: absolute;
  top: 49px;
  user-select: none;
  width: 100%;
  z-index: 2;
}

.SuggestionItem {
  cursor: pointer;
  padding: 14px 16px;

  &:hover,
  &:focus {
    background-color: #eee;
  }
}

.SuggestionLabel {
  font-size: medium;
  margin-left: 20px;
}

.AdditionalSuggestion {
  color: grey;
  font-size: small;
  margin-left: 10px;
}

.SuggestionIcon {
  color: grey;
  font-size: 25px !important;
  vertical-align: bottom;
}

.ToggleSuggestionListButton {
  color: gray;
  cursor: pointer;
  position: absolute;
  right: 15px;
  top: 15%;
  transition: transform ease-in-out 0.25s;

  &:global(.open) {
    transform: rotate(180deg);
  }

  &:hover,
  &:focus {
    color: var(--dark-green);
  }
}

```

### Final touches ✌️

We now have a fully styled and functional component! 🥳


Just let's add some icons as well.

I'm going to create another component inside, namely `SuggestionIcon`, that is going to receive `matchLevel` as the only prop and will return a proper icon based on it.

I'm going to use `FontAwesome` icons, so let's install it:

```bash
yarn add @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons @fortawesome/react-fontawesome
```

and then add these lines of code:

```tsx
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
  faRoad,
  faCity,
  faMapMarkerAlt,
  faAngleDown
} from '@fortawesome/free-solid-svg-icons';

interface SuggestionIconProps {
  matchLevel: string;
}

const SuggestionIcon: FC<SuggestionIconProps> = ({ matchLevel }) => {
  switch (matchLevel) {
    case 'street':
      return (
        <FontAwesomeIcon
          icon={faRoad}
          size="2x"
          className={Styles.SuggestionIcon}
          fixedWidth
        />
      );

    case 'city':
      return (
        <FontAwesomeIcon
          icon={faCity}
          size="2x"
          className={Styles.SuggestionIcon}
          fixedWidth
        />
      );

    default:
      return (
        <FontAwesomeIcon
          icon={faMapMarkerAlt}
          size="2x"
          className={Styles.SuggestionIcon}
          fixedWidth
        />
      );
  }
};
```

and then, simply add this component as follow:

```tsx
{data?.suggestions?.map(suggestion => (
              <li
                key={suggestion.locationId}
                className={Styles.SuggestionItem}
                onClick={() => {
                  setLocation(suggestion.label.replace(/<[^>]+>/g, ''));
                  setMenuOpen(false);
                  fetchLocation({
                    locationid: suggestion.locationId,
                    jsonattributes: 1
                  });
                }}
                tabIndex={1}
              >
                <SuggestionIcon matchLevel={suggestion.matchLevel} />
                <span
                  className={Styles.SuggestionLabel}
                  dangerouslySetInnerHTML={{ __html: suggestion.label }}
                />{' '}
                <span
                  className={Styles.AdditionalSuggestion}
                  dangerouslySetInnerHTML={{
                    __html: `${suggestion.address.county}, ${suggestion.address.state}, ${suggestion.address.country}`
                  }}
                />
              </li>
            ))}
```

Let's now add an icon to open and close the dropdown results menu as well:

```tsx
{data?.suggestions?.length && (
  <FontAwesomeIcon
    icon={faAngleDown}
    size="2x"
    className={cx(Styles.ToggleSuggestionListButton, {
      open: isMenuOpen
    })}
    onClick={() => {
      setMenuOpen(prevMenuOpen => !prevMenuOpen);
    }}
  />
)}

```

### All set!👌

Congratulations, you have just created an address autocomplete!

As you can see, with the help of `use-here-api` we were able to build a complex component with ease 😎

I've created a CodeSandbox where you can play with the code.

Feel free to fork and modify it as you wish! 😉

{% codesandbox address-autocomplete-0y3dh %}

*Note: please, do remember to replace `REACT_APP_HERE_APP_CODE` and `REACT_APP_HERE_APP_ID` in `.env`*

### What's next? 🤔

There is still place to further improve this component, like using throttling and/or debouncing techniques to avoid hampering the performance.

We can (actually should 😅) also enhance accessibility, following [ARIA design pattern for a dropdown select](https://www.w3.org/TR/wai-aria-practices/examples/listbox/listbox-collapsible.html).

Let me know in a comment down here if you would like me to expand upon these concepts! 🦄

---

Before leaving, I wanted to thank you for making it all the way down here! 🤗

Don't forget to `{...`❤️`}` if you enjoyed this post! 🤭

That's all for this post, see you next one and ... Happy hacking until then! 👨‍💻