Day 25: Adding Google Maps autocomplete search to a React app
Tags: googlemap, react, webdev, tutorial
Published: 2023-02-24 00:55:23

## TL;DR
To create an user experience with Google Maps place search like this:![A search box is shown with text cursor flashing in it. As characters "fushimi" are being entered in it, five search suggestions are shown below, changing each time a new character is entered. Then, the entered characters are deleted one by one, with search suggestions changing each time. Finally, when the entire text is deleted from the search box, no search suggestion is shown.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eot8isv6hlnmw5r8zxev.gif)
your React app needs the code as follows:
```js
// Each comment refers to the section of this article 
// which explains the code block that follows.

import {useMemo, useState} from 'react';
import {useCombobox} from 'downshift';

export const SearchBox = () => {
  // Section 5.1: to update UI as search suggestion list or Google Maps server status changes
  const [searchResult, setSearchResult] = useState({
    autocompleteSuggestions: [],
    status: '',
  });
  // Section 2.2: to make an API call to Google Maps server
  const google = window.google;
  const service = new google.maps.places.AutocompleteService();
  // Section 3: to save money for using Google Maps autocomplete search
  const sessionToken = useMemo(
    () => new google.maps.places.AutocompleteSessionToken(),
    [google.maps.places.AutocompleteSessionToken],
  );
  // Section 5.4: to make autocomplete search accessible
  const {
    getInputProps,
    getItemProps,
    getMenuProps,
  } = useCombobox({
    items: searchResult.autocompleteSuggestions,
    onInputValueChange: ({inputValue}) => {
      // Section 6.1: remove search suggestions when the search term is deleted in search box
      if (inputValue === '') {
        setSearchResult({
          autocompleteSuggestions: [],
          status: '',                 
        });
        return;
      }
      // Section 4.1: make an API call to Google Maps server
      service.getPlacePredictions({
        input: inputValue,         
        sessionToken: sessionToken,
        }, handlePredictions       
      );   
      // Section 4.2: process the response from Google Maps server
      function handlePredictions(predictions, status) {
        if (status === "OK") {
          const autocompleteSuggestions = predictions.map((prediction) => {
            return {
              id: prediction.place_id,
              name: {
                string: prediction.structured_formatting.main_text,
                // Section 7.3: to highlight the search term in suggested place names
                length: prediction.structured_formatting.main_text_matched_substrings[0]['length'],
                offset: prediction.structured_formatting.main_text_matched_substrings[0]['offset'],
              },
              address: {
                string: prediction.structured_formatting.secondary_text,
                // Section 7.3: to highlight the search term in suggested place address
                length: prediction.structured_formatting.secondary_text_matched_substrings[0]['length'],
                offset: prediction.structured_formatting.secondary_text_matched_substrings[0]['offset'],
              },
            };
          });
          // Section 5.2: update UI with new search suggestions
          setSearchResult({
            autocompleteSuggestions: autocompleteSuggestions,
            status: 'OK',
          })
        } else {
          // Section 5.2: update UI with an error message
          setSearchResult({
            autocompleteSuggestions: [],
            status: status,             
          });
        }
      }
    }
  })
  // Section 5.4: render UI in an accessible way
  return (
    <>
      <input 
        type="search"
        {...getInputProps()}
      >
      <ul
        {...getMenuProps()}
      >
        { // Section 5.3: show search suggestions
          searchResult.autocompleteSuggestions.length > 0
          ? searchResult.autocompleteSuggestions.map((item, index) => {
              return (
                <li
                  key={item.id}
                  {...getItemProps({
                    item,
                    index
                  })}
                >
                  {/* Section 7.3: highlight the search term in search suggestions */}
                  <p dangerouslySetInnerHTML={{__html: boldUserText(item.name)}}>
                  <p dangerouslySetInnerHTML={{__html: boldUserText(item.address)}}>
                </li>
              );
            })
          : null
        }
      </ul>
      {/* Section 6.2: show error messages */} 
      <SearchErrorMessage status={searchResult.status} />
    </>
  ) 
};
```
where the `boldUserText` function and the `SearchErrorMessage` component is given as follows:
```js
// Section 7.3: to highlight the search term in search suggestions
function boldUserText({ length, offset, string }) {
  if (length === 0 && offset === 0) {
    return string;
  }
  const userText = string.substring(offset, offset + length);
  const stringBefore = string.substring(0, offset);
  const stringAfter = string.substring(offset + length);
  return `${stringBefore}<b>${userText}</b>${stringAfter}`;
}
```
```js
export const SearchErrorMessage = ({status}) => {
  // Section 6.2: switch error messages in response to Google Maps server status
  return status === '' || status === 'OK' ? null : (
    <div role="alert">
      {
        status === 'ZERO_RESULTS' || status === 'INVALID_REQUEST' || status === 'NOT_FOUND' ? (
          <p>
            No place is found on the map. Try another search term.
          </p>
        {/* Section 6.3: show an error message when server access is rejected */}
        ) : status === 'OVER_QUERY_LIMIT' || status === 'REQUEST_DENIED' ? (
          <p>
            My Ideal Map is currently unable to use Google Maps search. Please contact us so we can fix the problem.
          </p>
        {/* Section 6.4: show an error message when Google Maps server is not responding */}
        ) : (
          <p>
            Google Maps server is down. <a href="https://status.cloud.google.com/maps-platform/products/i3CZYPyLB1zevsm2AV6M/history" target="_blank" rel="noreferrer">Please check its status</a>, and try again once they fix the problem (usually within a few hours).
          </p>
        )
      }
    </div>
  )
}
```
Then style the `<ul>` and `<li>` elements with CSS as you wish.

## 1. Introduction

_My Ideal Map_, a web app I’m building with React, allows the user to save the places of their own interest on Google Maps with rich text note. For this purpose, the app needs to incorporate the search feature of Google Maps so that the user can first find each place of their interest before saving it.

The [Places Library](https://developers.google.com/maps/documentation/javascript/places) of Google Maps JavaScript API allows you to embed Google Maps search into your own web app. 

As far as I know, there are only two articles on the web that explain how to incorporate the Places Library into React apps. Both [Kassym (2020)](https://betterprogramming.pub/the-best-practice-with-google-place-autocomplete-api-on-react-939211e8b4ce) and [Imoh (2022)](https://www.telerik.com/blogs/integrating-google-places-autocomplete-api-react-app) use the [`Autocomplete` widget](https://developers.google.com/maps/documentation/javascript/place-autocomplete#add-autocomplete), which comes with the predefined user interface. 

For more flexibility in UI design, however, we need to use the [`AutocompleteService` class](https://developers.google.com/maps/documentation/javascript/place-autocomplete#place_autocomplete_service), which only fetches the JSON data of autocomplete search suggestions from the Google Maps server. 

After quite a bit of trial and error, I have managed to incorporate Google Maps search into a React app with my own user interface (as shown in the GIF image above). Here’s how.

## 2. Setting up
### 2.1 Enabling the Place Library
The Place Library is not available to use by default. We need to enable it on Google Cloud Console.

How to do so is clearly explained in [Google Maps Platform documentation](https://developers.google.com/maps/documentation/javascript/places#enable_apis).

Once enabled, the Place Library needs to be added to the list of APIs that your Google Maps API key can be used for.

How to do so is also clearly explained in [Google Maps Platform documentation](https://developers.google.com/maps/documentation/javascript/places#add-places-api-to-the-api-keys-api-restrictions-list).

### 2.2 Loading the Places library
To embed Google Maps in a React app, we need the following code:
```js
import {useEffect, useRef} from 'react';
import {Loader} from '@googlemaps/js-api-loader';
export function HomePage() {
  const googlemap = useRef(null);
  useEffect(() => {
    const loader = new Loader({
      apiKey: process.env.NEXT_PUBLIC_API_KEY,
      version: 'weekly',
    });
    let map;
    loader.load().then(() => {
      map = new google.maps.Map(googlemap.current, {
        center: {lat: -34.397, lng: 150.644}, // or anywhere you want to show on the map by defaul
        zoom: 8, // or any other zoom level
      });
    });
  });
  return (
    <div id="map" ref={googlemap} />
  );
}
```
where I use Next.js’s environment variable `NEXT_PUBLIC_API_KEY` to keep the API key secret  (see my article, [Kudamatsu (2021)](https://medium.com/web-dev-survey-from-kyoto/3-gotchas-of-google-maps-api-when-used-with-next-js-and-eslint-dba627c9657d), for more detail).

To use the Places Library, we need to change the options for the `Loader` class as follows:
```js
const loader = new Loader({
  apiKey: process.env.NEXT_PUBLIC_API_KEY,
  version: 'weekly',
  libraries: ['places'], // ADDED
});
```
Surprisingly, you cannot find this code snippet in Google Maps Platform documentation on the Places Library. I only found it in the [GitHub repo for `googlemaps/js-api-loader`](https://github.com/googlemaps/js-api-loader#example).

### 2.3 Instantiate AutocompleteService class

Now the Places Library is available as `google.maps.places`. For flexible UI design on autocomplete search, we need to use its `AutocompleteService` class:
```js
const google = window.google;
const service = new google.maps.places.AutocompleteService();
```
where the first line of code is to silence ESLint, which will otherwise complain that `google` is used without being defined (see [Abramov 2017](https://stackoverflow.com/a/43718073/11847654)) for detail). The second line of code comes from [Google Maps Platform documentation](https://developers.google.com/maps/documentation/javascript/place-autocomplete#place_autocomplete_service).

## 3. Session tokens

### 3.1 How Google charges us for using autocomplete search

The autocomplete search of Google Maps is NOT free of charge. By default, each time the user enters a character to retrieve autocomplete suggestions, it costs 0.00283 US dollars (source: [Google Maps Platform documentation](https://developers.google.com/maps/documentation/places/web-service/usage-and-billing#ac-per-request)). In addition, when the user clicks one of the autocomplete suggestions, it costs 0.017 US dollars (source: [Google Maps Platform documentation](https://developers.google.com/maps/documentation/places/web-service/usage-and-billing#places-details)).

But there is a cheaper price plan. If the request to the Google Maps server is sent with a _session token_, then, it will only cost 0.017 US dollars for the the _whole_ process of autocomplete search: entering search words _and_ choosing an autocomplete suggestion (source: [Google Maps Platform documentation](https://developers.google.com/maps/documentation/places/web-service/usage-and-billing#ac-with-details-session)). If the user ends up not choosing any of autocomplete suggestions, it will also cost 0.017 US dollars (source: [Google Maps Platform documentation](https://developers.google.com/maps/documentation/places/web-service/usage-and-billing#ac-no-details-session)).

To benefit from this price plan, a single _session token_ needs to be sent to the server during the whole process of autocomplete search. And a new session token must be used for a new search; otherwise the default price plan (charged per character to enter) will be applied (source: [Google Maps Platform documentation](https://developers.google.com/maps/documentation/places/web-service/session-tokens)).

Consequently, we first need to generate a session token before sending the user's search text to the Google Maps server.

### 3.2 Generating session tokens for React apps

To generate a session token, [Google Maps Platform documentation](https://developers.google.com/maps/documentation/javascript/place-autocomplete#session_tokens) teaches us to code like this:

```js
const sessionToken = new google.maps.places.AutocompleteSessionToken();
```

However, this line of code doesn’t really work with React apps. The search term needs to be managed as a React state so that the search box gets re-rendered each time the user enters a character in it. Consequently, whenever the user changes the search text, the whole code of the search box component gets executed, with a new session token generated and sent to the Google Maps server. This kills the whole idea of using a session token to save money.

To make sure the same session token will be sent to the server until the user finishes a search process, we need to use [`useMemo()`](https://beta.reactjs.org/reference/react/useMemo):
```js
const sessionToken = useMemo(
  () => new google.maps.places.AutocompleteSessionToken(),
  [google.maps.places.AutocompleteSessionToken],
);
```

This way, even if the React component gets rerendered due to changes in the search term, the session token will remain the same.

On the other hand, when the search process is over, my React app dismounts the search box component. Consequently, when the user starts a new search process, a new search box component is rendered with a new session token issued by the above line of code. (If the search box needs to be shown all the time, a different solution is needed for which I don’t know what to do.)

## 4. Interacting with API

Now we’re ready to interact with the Google Maps server. First, we send the search term to the server (Section 4.1). Then, we process the response returned by the server (Section 4.2).

### 4.1 Making an API call
In a simple setting, a request for autocomplete search can be sent to the server with the following code (source: [Google Maps Platform documentation](https://developers.google.com/maps/documentation/javascript/place-autocomplete#session_tokens)):
```js
service.getPlacePredictions(
  {
    input: 'pizza near Syd',
    sessionToken: sessionToken,
  },
  handlePredictions,
);
```
where
- `service` is an instance of `AutocompleteService` class (see Section 2.3 above)
- `input` specifies the search term entered by the user
- `sessionToken` is the one generated with `AutocompleteSessionToken()` (see Section 3.2 above)
- `handlePredictions` is a function to process autocomplete search results (to be defined below)

There are [a set of optional parameters for the `getPlacePredictions()` method](https://developers.google.com/maps/documentation/javascript/reference/places-autocomplete-service#AutocompletionRequest). Among them, `location` and `radius` can be set so that search suggestions will be biased towards a certain area such as the user’s city of residence (see [Google Maps Platform documentation](https://developers.google.com/maps/documentation/places/web-service/autocomplete#location_biasing) for detail). 

I’m not sure whether I want this feature. If the user is planning a trip, biasing towards where they live is certainly not helpful. Allowing the user to change the location bias will just complicate the user experience of searching a place. 

---

The `AutocompleteService` class has another function called `getQueryPredictions()`, which will retrieve not only autocomplete suggestions but also _suggested search terms_ like “pizza in New York” when the user just enters “pizza in new” (source: [Google Maps Platform docs](https://developers.google.com/maps/documentation/javascript/place-autocomplete#place_autocomplete_service)). 

For the purpose of _My Ideal Map_, this extra suggestion is unnecessary. The app is for those users who knows a place of their interest, not for those who rely on Google Maps to suggest where to visit.

### 4.2 Processing server responses

We now need to specify the `handlePredictions` function to process the autocomplete suggestion data returned from the Google Maps server.

Once we send a request by calling `getPlacePredictions()`, we will receive two sets of data, `predictions` and `status`. So the `handlePredictions` function should take these two variables as its arguments:

```js
function handlePredictions(predictions, status) {}
```

The `status` variable indicates whether the API request is successful (see [Google Maps Platform documentation](https://developers.google.com/maps/documentation/javascript/reference/places-service#PlacesServiceStatus)). If successful, its value is `"OK"`. So I code as follows:
```js
function handlePredictions(predictions, status) {
  if (status === "OK") {
    // handle autocomplete suggestions
  } else {
    // handle error
  }
}
```

The `predictions` variable refers to an array of objects each of which contains an autocomplete suggestion. Here is an example of those objects (it’s one of my favorite museums in Kyoto):
```json
{
  "description" : "Fukuda Art Museum, 3-16 Sagatenryuji Susukinobabacho, Ukyo Ward, Kyoto, Japan",
  "matched_substrings" : [
    {
        "length" : 8,
        "offset" : 0
    },
    {
        "length" : 33,
        "offset" : 19
    }
  ],
  "place_id" : "ChIJfwxa4v-pAWARQ-R4_cVAgAc",
  "reference" : "ChIJfwxa4v-pAWARQ-R4_cVAgAc",
  "structured_formatting" : {
    "main_text" : "Fukuda Art Museum",
    "main_text_matched_substrings" : [
        {
          "length" : 8,
          "offset" : 0
        }
    ],
    "secondary_text" : "3-16 Sagatenryuji Susukinobabacho, Ukyo Ward, Kyoto, Japan",
    "secondary_text_matched_substrings" : [
        {
          "length" : 33,
          "offset" : 0
        }
    ]
  },
  "terms" : [
    {
        "offset" : 0,
        "value" : "Fukuda Art Museum"
    },
    {
        "offset" : 19,
        "value" : "3-16 Sagatenryuji Susukinobabacho"
    },
    {
        "offset" : 54,
        "value" : "Ukyo Ward"
    },
    {
        "offset" : 65,
        "value" : "Kyoto"
    },
    {
        "offset" : 72,
        "value" : "Japan"
    }
  ],
  "types" : [ "museum", "point_of_interest", "establishment" ]
}
```
For the definition of each property, see [Google Maps Platform documentation](https://developers.google.com/maps/documentation/javascript/reference/places-autocomplete-service#AutocompletePrediction). 

For the purpose of _My Ideal Map_, all I need is `place_id` and `structured_formatting` properties.

When the user selects one of the autocomplete suggestions, _My Ideal Map_ makes another API call to the Google Maps server so that its location will be displayed on the map along with additional pieces of information on the place. To do so, we need `place_id`.

To select one of the autocomplete suggestions, the user needs to see the name of the place and its address (the address is crucial as there are the places with the same name in different cities). The `structured_formatting` property contains an object whose `main_text` and `secondary_text` properties provide the name and address, respectively, of the place.

Consequently, I transform the `predictions` array as follows:
```js
function handlePredictions(predictions, status) {
  if (status === "OK") {
    // ADDED FROM HERE
    const autocompleteSuggestions = predictions.map((prediction) => {
      return {
        id: prediction.place_id,
        name: {
          string: prediction.structured_formatting.main_text,
        },
        address: {
          string: prediction.structured_formatting.secondary_text,
        },
      };
    });
    // ADDED UNTIL HERE
  } else {
    // handle error
  }
}
```
You might wonder why I use `name.string` and `address.string`, rather than `name` and `address`, to store the name and address of a place. Please be patient. I'll explain it in Section 7 below.

The `autocompleteSuggestions` array will then be used to render the list of suggested places in response to the user’s search text.

To do so, I use React’s state with the help of a library called Downshift.

## 5. Rendering autocomplete suggestions with React
### 5.1 Initializing the state
First, I create a state variable called `searchResult` with the `useState` hook of React:
```js
const [searchResult, setSearchResult] = useState({
  autocompleteSuggestions: [],
  status: '',
}); 
```
This state variable is an object with two properties. One of them,  `autocompleteSuggestions`, is to store the array of suggestions returned by Google Maps API. The other property, `status`, will store the `status` variable returned from Google Maps API. 

The `status` property is needed as part of the React state so that I can update the UI with an error message when the Google Maps server returns a value of `status` other than `"OK"`.

And I manage these two states as one object, rather than two separate state variables, because they get updated at the same time (you’ll see this in action shortly). 

### 5.2 Updating the state
With `setSearchResult` to update the React state, the `handlePredictions` function is revised as follows:
```js
function handlePredictions(predictions, status) {
  if (status === "OK") {
    const autocompleteSuggestions = predictions.map((prediction) => {
      return {
        id: prediction.place_id,
        name: {
          string: prediction.structured_formatting.main_text,
        },
        address: {
          string: prediction.structured_formatting.secondary_text,
        },
      };
    });
    // ADDED FROM HERE
    setSearchResult({
      autocompleteSuggestions: autocompleteSuggestions,
      status: 'OK',
    })
    // ADDED UNTIL HERE
  } else {
    // ADDED FROM HERE
    setSearchResult({
      autocompleteSuggestions: [],
      status: status,
    })
    // ADDED UNTIL HERE
  }
}
```
When search is successful (`status==="OK"`), the React state gets updated with the `autocompleteSuggestions` array and the string of `"OK"`. Otherwise, it is updated with an empty array and the `status` variable. You’ll shortly see why this way of updating the React state is convenient. 

### 5.3 Rendering autocomplete suggestions
With the array of autocomplete suggestions, saved as `searchResult.autocompleteSuggestions`, `<li>` elements can be rendered with React as follows:
```js
searchResult.autocompleteSuggestions.length > 0
  ? searchResult.autocompleteSuggestions.map((item) => {
      return (
        <li key={item.id}>
          <p>{item.name.string}</p>
          <p>{item.address.string}</p>
        </li>
      );
    })
  : null
```
First, I check whether the array is empty by looking at its `length` property. If it’s `0` (which is the default value, that is, before the user types any character in the search box), I don’t render any `<li>` element. (This is why, in the previous subsection, I set `searchResult.autocompleteSuggestions` to be an empty array when `status` is not `"OK"`: in such a case, we don’t want to render `<li>` elements).

Second, I use the place ID for the `key` prop, which is necessary for React to remember each `<li>` element across re-rendering (see [Dodds 2017](https://egghead.io/lessons/react-use-the-key-prop-when-rendering-a-list-with-react) for demo). 

Third, I insert each of the place name and its address into the `<p>` element. These are not paragraphs, but I don’t see any other HTML elements more appropriate. The `<address>` element is for the contact information, not for addresses in general (see [Section 4.3.10 of HTML Living Standard](https://html.spec.whatwg.org/multipage/sections.html#the-address-element)).

---

There remain two things still missing in the code built so far. First, we need to make an API call to the Google Maps server whenever the user changes the search term in the search box. Second, the whole user experience of autocomplete search needs to be accessible.

For implementing these two, I use a library called Downshift, which makes life a lot easier than coding from scratch (see [**Day 24 of this blog series**](https://dev.to/masakudamatsu/day-24-making-autocomplete-search-accessible-for-react-apps-with-downshift-1h6n) for detail).

### 5.4 Using Downshift

First, import the `useCombobox` hook from the `downshift` NPM module:
```js
import {useCombobox} from 'downshift';
```

Then, specify the array of autocomplete suggestions as the `items` option:
```js
useCombobox({
  items: searchResult.autocompleteSuggestions,
})
```

The `useCombobox` hook returns what they call _prop getters_:
```js
const {
  getInputProps,
  getItemProps,
  getMenuProps,
} = useCombobox({
  items: searchResult.autocompleteSuggestions,
})
```
We use these _prop getters_ to (1) add ARIA attributes to `<input>`, `<ul>`, and `<li>` elements and (2) make these elements work in tandem to provide accessibility:
```js
return (
  <>
    <input 
      type="search"
      {...getInputProps()}
    >
    <ul
      {...getMenuProps()}
    >
      {searchResult.autocompleteSuggestions.length > 0
        ? searchResult.autocompleteSuggestions.map((item, index) => {
            return (
              <li
                key={item.id}
                {...getItemProps({
                  item,
                  index
                })}
              >
                <p>{item.name.string}</p>
                <p>{item.address.string}</p>
              </li>
            );
          })
        : null
      }
    </ul>   
  </>
)
```
These _prop getters_ return an object of HTML attributes, which is why we use the notations like `{...getInputProps()}`. 

The `getItemProps()` requires as arguments both the array item (`item`) and its index (`index`). This is why we add `index` as the argument for a function specified in the `.map()` array method.

There are a few more things to code for using the `useCombobox` hook of Downshift, but I just refer you to [**Day 24 of this blog series**](https://dev.to/masakudamatsu/day-24-making-autocomplete-search-accessible-for-react-apps-with-downshift-1h6n) (see Section 3).

---

Now it’s time to code for making an API call to the Google Maps server whenever the user types something in the search box. All we need to do is to specify as the `useCombobox` hook’s `onInputValueChange` option the series of functions to execute for making an API call (see Sections 4.1, 4.2 and 5.2 above):
```js
const {
  getInputProps,
  getItemProps,
  getMenuProps,
} = useCombobox({
  items: searchResult.autocompleteSuggestions,
  // ADDED FROM HERE
  onInputValueChange: ({inputValue}) => {
    // Section 4.1
    service.getPlacePredictions({
      input: inputValue,         
      sessionToken: sessionToken,
      }, handlePredictions       
    );                     
    // Section 4.2      
    function handlePredictions(predictions, status) {
      if (status === "OK") {
        const autocompleteSuggestions = predictions.map((prediction) => {
          return {
            id: prediction.place_id,
            name: {
              string: prediction.structured_formatting.main_text,
            },
            address: {
              string: prediction.structured_formatting.secondary_text,
            },
          };
        });
        // Section 5.2
        setSearchResult({
          autocompleteSuggestions: autocompleteSuggestions,
          status: 'OK',
        })
      } else {
        // Section 5.2
        setSearchResult({
          autocompleteSuggestions: [],
          status: status,             
        });
      }
    }
  }
  // ADDED UNTIL HERE
})
```
Downshift assigns to the `inputValue` variable the text the user has entered into the search box. It is then used as the `input` parameter for the `getPlacePredictions` function so that the Google Maps server retrieves autocomplete suggestions in response to the user input.

In the code above, I use _hoisting_ for `handlePredictions`. When a function is defined with the `function` keyword, it can be used before it is defined ([MDN Contributors 2023](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions#function_hoisting)). As far as I know, this way of coding is common in Python, but not in JavaScript. However, in this particular case where the `getPlacePredictions` function specifies a callback function as its argument, I believe it improves code readability: the functions are executed in the order of being written in the code.

### 5.5 Putting all the code together so far
With the code written so far, whenever the user types a character in the search box, the app renders the list of autocomplete suggestions. We still need to handle edge cases and to improve the user interface. But let's take stock by putting all the code together so far, with references to the relevant sections as comments:
```js
import {useMemo, useState} from 'react';
import {useCombobox} from 'downshift';

export const SearchBox = () => {
  // Section 5.1
  const [searchResult, setSearchResult] = useState({
    autocompleteSuggestions: [],
    status: '',
  });
  // Section 2.2
  const google = window.google;
  const service = new google.maps.places.AutocompleteService();
  // Section 3
  const sessionToken = useMemo(
    () => new google.maps.places.AutocompleteSessionToken(),
    [google.maps.places.AutocompleteSessionToken],
  );
  // Section 5.4
  const {
    getInputProps,
    getItemProps,
    getMenuProps,
  } = useCombobox({
    items: searchResult.autocompleteSuggestions,
    onInputValueChange: ({inputValue}) => {
      // Section 4.1
      service.getPlacePredictions({
        input: inputValue,         
        sessionToken: sessionToken,
        }, handlePredictions       
      );   
      // Section 4.2
      function handlePredictions(predictions, status) {
        if (status === "OK") {
          const autocompleteSuggestions = predictions.map((prediction) => {
            return {
              id: prediction.place_id,
              name: {
                string: prediction.structured_formatting.main_text,
              },
              address: {
                string: prediction.structured_formatting.secondary_text,
              },
            };
          });
          // Section 5.2
          setSearchResult({
            autocompleteSuggestions: autocompleteSuggestions,
            status: 'OK',
          })
        } else {
          // Section 5.2
          setSearchResult({
            autocompleteSuggestions: [],
            status: status,             
          });
        }
      }
    }
  })
  // Section 5.4
  return (
    <>
      <input 
        type="search"
        {...getInputProps()}
      >
      <ul
        {...getMenuProps()}
      >
        { // Section 5.3 
          searchResult.autocompleteSuggestions.length > 0
          ? searchResult.autocompleteSuggestions.map((item, index) => {
              return (
                <li
                  key={item.id}
                  {...getItemProps({
                    item,
                    index
                  })}
                >
                  <p>{item.name.string}</p>
                  <p>{item.address.string}</p>
                </li>
              );
            })
          : null
        }
      </ul>
    </>
  ) 
};
```
With this code snippet, we can freely style the list of autocomplete suggestions like this:
![A search box with semi-circle edges on both sides shows the search term _kyoto_, below which five boxes of the shape similar to the search box show autocomplete suggestions: Kyoto, Kyoto Station, Kyoto University, Kyoto Katsugyu, and Kyoto Railway Museum. In these suggestions, the phrase _Kyoto_ is highlighted in bold.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/up4xxv6tbuh09kinq96k.png)**A screenshot of My Ideal Map in development**

where the search term is highlighted in bold with the additional set of code described in Section 7 below.

## 6. Handling edge cases
We need to handle four edge cases: (i) the user deletes the whole search term; (ii) no search result is returned; (iii) access to the Google Maps server is denied; (iv) the Google Maps server is down.

Let's look at each of these four edge cases.

### 6.1 Deleting the whole search term
The code snippet in Section 5.5 leaves the list of autocomplete suggestions visible when the user deletes the entire search term. This is because the React state isn’t reset when the search box field becomes empty.

To handle this case, we need to revise the `useCombobox` hook’s `onInputValueChange` prop, to reset the React state when `inputValue` is an empty string:
```js
const {
  getInputProps,
  getItemProps,
  getMenuProps,
} = useCombobox({
  items: searchResult.autocompleteSuggestions,
  onInputValueChange: ({inputValue}) => {
    // ADDED FROM HERE
    if (inputValue === '') {
      setSearchResult({
        autocompleteSuggestions: [],
        status: '',                 
      });
      return;
    }
    // ADDED UNTIL HERE
    service.getPlacePredictions({
      input: inputValue,         
      sessionToken: sessionToken,
      }, handlePredictions       
    );   
    function handlePredictions(predictions, status) {
      // Omitted for brevity
    }
  }
});
```

### 6.2 No search result

The code snippet in Section 5.5 above does not give any message when there is no search result. The user is then unsure of whether there is no search result or the Google Maps server is not responding. That is not a great user experience.

First of all, what kind of message should I display in case of no search result?

[Whitenton (2014)](https://www.nngroup.com/articles/search-no-results-serp/) recommends:
1. Clearly explain that there are no matching results.
2. Offer starting points for moving forward.
3. Don't mock the user.

Food.com, mentioned by [Whitenton (2014)](https://www.nngroup.com/articles/search-no-results-serp/) as a good example, provides the following message for no search result:
> **Sorry, no results were found.** ... Check your spelling. Try more general words. Try different words that mean the same thing.

That was back in 2014. [Food.com today gives the following message for no search result](https://www.food.com/search/achzchoihozcuh):
> **OOPS! NO MATCHES.** How about digging into some of our most popular stuff instead?

I guess I can go with a simple message like this:
> No place is found on the map. Try another search term.

---

Now, how can we display this message in case of no search result? 

When the Google Maps server returns no search result, the `status` variable takes the value of `"ZERO_RESULTS"` (see [Google Maps Platform documentation](https://developers.google.com/maps/documentation/javascript/reference/places-service#PlacesServiceStatus)).

Therefore, we can check the statement:
```js
status === `ZERO_RESULTS`
```
and if it is `true`, render the following HTML element:
```html
<div role="alert">
  <p>
    No place is found on the map. Try another search term.
  </p>
</div>
```
I use `role="alert"` so that the screen reader will read out the message when this `<div>` element is inserted into the DOM (see [Whiting 2017](https://webaim.org/blog/aria-cause-solution/) for how to use `role="alert"` properly).

To implement this behavior with React, I create a separate component named `SearchErrorMessage` which takes `status` as a prop:
```js
export const SearchErrorMessage = ({status}) => {
  return status === '' || status === 'OK' ? null : (
    <div role="alert">
      {
        status === 'ZERO_RESULTS' ? (
          <p>
            No place is found on the map. Try another search term.
          </p>
        ) : null
      }
    </div>
  )
}
```
where it renders nothing when `status` is an empty string (the initial state) or `"OK"` (at least one search result is returned). Otherwise, it renders `<div role="alert">`, the inside of which the value of `status` is checked if it’s `"ZERO_RESULTS"`. If so, the message for no search result is rendered as a `<p>` element. This way, we can include some link text in the message, if necessary.

When the `status` variable takes the string value of `"INVALID_REQUEST"` (this request is invalid) or `"NOT_FOUND"` (the place referenced was not found), what the user needs to do is to try another search term. So, even though it is not strictly about the "no search result" situation, I think we can use the same message.

So I add these cases to the above code:
```js
export const SearchErrorMessage = ({status}) => {
  return status === '' || status === 'OK' ? null : (
    <div role="alert">
      {
        // REVISED FROM HERE
        status === 'ZERO_RESULTS' || status === 'INVALID_REQUEST' || status === 'NOT_FOUND' ? (
        // REVISED UNTIL HERE
          <p>
            No place is found on the map. Try another search term.
          </p>
        ) : null
      }
    </div>
  )
}
```

Then, import and use this component into the `SearchBox` component as follows:
```js
return (
  <>
    <input 
      type="search"
      {...getInputProps()}
    >
    <ul
      {...getMenuProps()}
    >
      {/* Omitted for brevity */}
    </ul>
    <SearchErrorMessage status={searchResult.status} /> {/* ADDED */}
  </>
);
```

### 6.3 Server access is denied
If `status` is `"OVER_QUERY_LIMIT"` (The application has gone over its request quota) or `"REQUEST_DENIED"` (The application is not allowed to use the PlacesService), this is a situation where it is the fault of my app, _My Ideal Map_, rather than Google Maps. 

So the appropriate message is to ask the user to contact me, something like this:
> My Ideal Map is currently unable to use Google Maps search. Please contact us so we can fix the problem.

To show this message when appropriate, revise the `SearchErrorMessage` component as follows:
```js
export const SearchErrorMessage = ({status}) => {
  return status === '' || status === 'OK' ? null : (
    <div role="alert">
      {
        status === 'ZERO_RESULTS' || status === 'INVALID_REQUEST' || status === 'NOT_FOUND' ? (
          <p>
            No place is found on the map. Try another search term.
          </p>
        {/* ADDED FROM HERE */}
        ) : status === 'OVER_QUERY_LIMIT' || status === 'REQUEST_DENIED' ? (
          <p>
            My Ideal Map is currently unable to use Google Maps search. Please contact us so we can fix the problem.
          </p>
        {/* ADDED UNTIL HERE */}
        ) : null
      }
    </div>
  )
}
```

I need to add a contact form to this message later in the process of making the app.

### 6.4 Google Maps server is down
The final edge case is when the Google Maps server is not responding to the API call. In this case, the `status` variable takes the string value of `"UNKNOWN_ERROR"`.

Also, if someone hacks the Google Maps server, the `status` variable may take other values than [the ones specified in the documentation](https://developers.google.com/maps/documentation/javascript/reference/places-service#PlacesServiceStatus).

So I revise the `SearchErrorMessage` component as follows:
```js
export const SearchErrorMessage = ({status}) => {
  return status === '' || status === 'OK' ? null : (
    <div role="alert">
      {
        status === 'ZERO_RESULTS' || status === 'INVALID_REQUEST' || status === 'NOT_FOUND' ? (
          <p>
            No place is found on the map. Try another search term.
          </p>
        ) : status === 'OVER_QUERY_LIMIT' || status === 'REQUEST_DENIED' ? (
          <p>
            My Ideal Map is currently unable to use Google Maps search. Please contact us so we can fix the problem.
          </p>
        {/* REVISED FROM HERE */}
        ) : (
          <p>
            Google Maps server is down. <a href="https://status.cloud.google.com/maps-platform/products/i3CZYPyLB1zevsm2AV6M/history" target="_blank" rel="noreferrer">Please check its status</a>, and try again once they fix the problem (usually within a few hours).
          </p>
        )
        {/* REVISED UNTIL HERE */}
      }
    </div>
  )
}
```
where the link points to the Google Maps Platform page on incidents reported on Places API. Since it is inconvenient for the user to leave the app to visit an external website, clicking the link text will open the site in a new tab with `target="_blank"`, which should always be coupled with `rel="noreferrer"` for security reasons (otherwise [ESLint complains](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-no-target-blank.md)).

I do not check the statement `status === "UNKNOWN_ERROR"` in the above code because all the other cases are already handled in the code by the time the browser reaches these lines of code.

---
I can improve the exact phrase of the messages to appear in the four edge cases described above. I would be happy to hear you in a comment to this article if you have a better idea.

### 6.5 None of the suggestions is relevant

The last edge case is what I do not know how to handle.

The Google Maps server returns _up to five_ search suggestions in response to the search term (see [Google Maps Platform documentation](https://developers.google.com/maps/documentation/places/web-service/autocomplete#PlacesAutocompleteResponse)). And there seems no way to increase the upper limit (see [a StackOverflow discussion on this matter](https://stackoverflow.com/q/20785537)).

Consequently, none of the five suggestions may be what the user is searching for. The user is likely to expect to see more suggestions by scrolling down. But the Google Maps server doesn’t allow us to implement it in an app.

We could tell the user that we cannot show more than five suggestions, if the user doesn’t do anything for, say, three seconds after five search suggestions get displayed.

But that is an intrusive user experience. The user may find what they are looking for, but just get interrupted by a phone call or something. Showing a message in this case can confuse the user.

Any idea?

## 7. Styling autocomplete suggestion text
The code written so far will do a decent job to provide the user with the autocomplete search feature of Google Maps.

For an additional improvement of user experience, however, we can highlight the search term that appears in the autocomplete suggestions. 

### 7.1 UI design considerations

Writing about UX design of search suggestions, [Moran 2018](https://www.nngroup.com/articles/site-search-suggestions/) recommends visually differentiating those characters already typed by the user and those suggested by the app:
> ...each suggestion has two parts: the characters already typed by the user, and the characters suggested by the system to complete the query. It’s important to use different visual styles to show which characters fall into each category. You can use bolding, italics, color, or indenting to communicate the differences between these parts. Visual differentiation helps users understand why the suggestions are shown and scan the available options.

A thorny issue is which type of characters to highlight, the characters already typed by the user or those suggested by the system. The same article recommends the following:
> If your suggested search feature only appends characters to the end of the user’s text to finish the query, then you should highlight the suggested characters. If, instead, your suggested search feature will suggest popular queries that contain the user’s text anywhere in the query, it’s best to highlight the user’s query.—[Moran (2018)](https://www.nngroup.com/articles/site-search-suggestions/)

Google Maps’s search falls into the second case: the user’s text is contained anywhere in a suggested place name.

So I want to highlight the user’s text.

### 7.2 What Google Maps API gives us
Google Maps API returns not only place names and addresses, but also
- `length`: the number of characters of the user’s text included in a place name or address
- `offset`: the number of characters in a place name or address that precede the user’s text

For example, imagine the user types `arashiyama garden`, which is 17 characters long (including a space).

One of the autocomplete suggestions will be `Bread, Espresso & Arashiyama Garden` (one of my favorite cafes in Kyoto). In this case, the user’s text starts after 18 characters (including spaces) of `Bread, Espresso & `.

So the `length` is 17, and the `offset` is 18.

In Section 4.2 above, I gave an example of the object of an autocomplete suggestion returned by Google Maps API. Let me give an excerpt of it:
```json
"structured_formatting" : {
  "main_text" : "Fukuda Art Museum",
  "main_text_matched_substrings" : [
      {
        "length" : 8,
        "offset" : 0
      }
  ],
  "secondary_text" : "3-16 Sagatenryuji Susukinobabacho, Ukyo Ward, Kyoto, Japan",
  "secondary_text_matched_substrings" : [
      {
        "length" : 33,
        "offset" : 0
      }
  ]
},
```
The `length` and `offset` values are given in the `main_text_matched_substrings` and `secondary_text_matched_substrings` properties of the `structured_formatting` property for the place name and its address, respectively. 

### 7.3 Implementation
To extract the `length` and `offset` values, let’s revise the `handlePredictions` function in Section 4.2 as follows:
```js
function handlePredictions(predictions, status) {
  if (status === "OK") {
    const autocompleteSuggestions = predictions.map((prediction) => {
      return {
        id: prediction.place_id,
        name: {
          string: prediction.structured_formatting.main_text,
          // ADDED FROM HERE
          length: prediction.structured_formatting.main_text_matched_substrings[0]['length'],
          offset: prediction.structured_formatting.main_text_matched_substrings[0]['offset'],
          // ADDED UNTIL HERE
        },
        address: {
          string: prediction.structured_formatting.secondary_text,
          // ADDED FROM HERE
          length: prediction.structured_formatting.secondary_text_matched_substrings[0]['length'],
          offset: prediction.structured_formatting.secondary_text_matched_substrings[0]['offset'],
          // ADDED UNTIL HERE
        },
      };
    });
    setSearchResult({
      autocompleteSuggestions: autocompleteSuggestions,
      status: 'OK',
    })
  } else {
    setSearchResult({
      autocompleteSuggestions: [],
      status: status,             
    });
  }
}
```

Given that each autocomplete suggestion takes the form of an object with `string`, `length`, and `offset` properties, I create a custom function to bold the user’s text:
```js
function boldUserText({ length, offset, string }) {
  if (length === 0 && offset === 0) {
    return string;
  }
  const userText = string.substring(offset, offset + length);
  const stringBefore = string.substring(0, offset);
  const stringAfter = string.substring(offset + length);
  return `${stringBefore}<b>${userText}</b>${stringAfter}`;
}
```
If both `length` and `offset` are 0, then there is no user’s text. So just return the original `string`. Otherwise, extract the user’s text, the text before, and the text after by using the `.substring()` string method of JavaScript along with the `length` and `offset` values. Finally, combine the three with the user’s text wrapped with the `<b>` tag. This is what `boldUserText()` returns.

The `.substring()` method works as follows. When it takes two arguments, `a` and `b`, it removes the first `a` characters, keeps the subsequent `b` characters, and drop the rest. When it takes only one argument, say, `c`, it removes the first `c` characters and keep the rest (see [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substring) for detail).

I use `<b>` to bold user’s text. According to the HTML Living Standard:
> The b element represents a span of text to which attention is being drawn for utilitarian purposes without conveying any extra importance and with no implication of an alternate voice or mood, such as key words in a document abstract, product names in a review, actionable words in interactive text-driven software, or an article lede.—[HTML Living Standard, section 4.5.21](https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-b-element)

In our case, the bold text is meant to draw the user’s attention so that they will know which part of autocomplete suggestions includes the text they have entered in the search box. That’s exactly what the `<b>` element stands for.

It shouldn’t be `<strong>`, because:
> The strong element represents strong importance, seriousness, or urgency for its contents.—[HTML Living Standard, section 4.5.3](https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-strong-element)

In our case, the bold text does not convey any extra significance.

Now, how should we use the `boldUserText` function to render HTML? Given that the returned value from this helper function is not string but JSX (i.e., containing HTML tags), we cannot code like this:
```js
// DOES NOT WORK!
{
  searchResult.autocompleteSuggestions.length > 0
  ? searchResult.autocompleteSuggestions.map((item, index) => {
      return (
        <li
          key={item.id}
          {...getItemProps({
            item,
            index
          })}
        >
          <p>{boldUserText(item.name)}</p>
          <p>{boldUserText(item.address)}</p>
        </li>
      );
    })
  : null
}
```

Instead, we need to use the React's prop called `dangerouslySetInnerHTML` (see [React docs](https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml)):

```js
{
  searchResult.autocompleteSuggestions.length > 0
    ? searchResult.autocompleteSuggestions.map((item, index) => {
      return (
        <li key={item.id} {...getItemProps({ item, index })}>
          <p dangerouslySetInnerHTML={{__html: boldUserText(item.name)}}>
          <p dangerouslySetInnerHTML={{__html: boldUserText(item.address)}}>
        </li>
      );
    });
}
```
Now each autocomplete suggestion highlights the search term that the user has entered in the search box!
![Characters "fushim" are shown in the search box below which five search suggestions are displayed with "fushim" highlighted in bold](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g69w03qnpopey5aryanb.png)**The search term "fushim" is highlighted in bold** (a screenshot of My Ideal Map in development)

As you see in the above screenshot, a few parts of the addresses are also highlighted even though they don’t contain the search term. This is Google Maps’s fault. The `offset` and `length` values do not necessarily reflect where the search term is included. It is a bug that I need to fix in the future, but not critical enough for a minimum viable product.

## 8. Why Demo is unavailable
I wish I could give you the link to the demo of the entire code described in this article. However, Google charges me for accessing to Google Maps API, as described in Section 3.1 above. 

Sorry.

## 9. Next up
The next step is to show on the embedded Google Maps the place the user selects out of the autocomplete suggestions rendered in this article. 

Here you go: [**Day 26 of this blog series**](https://dev.to/masakudamatsu/day-26-integrating-google-maps-search-with-a-react-app-200p)

## References
Dan Abramov (2017) “[As mentioned in the user guide, you need to explicitly read any global variables from `window`...](https://stackoverflow.com/a/43718073/11847654)”, _Stack Overflow_, May 1, 2017.

Dodds, Kent C. (2017) “[Use the key prop when Rendering a List with React](https://egghead.io/lessons/react-use-the-key-prop-when-rendering-a-list-with-react)”, _The Beginner’s Guide to React V1_, 2017.

Imoh, Chinedu. (2022) “[Integrating Google Places Autocomplete API in a React App](https://www.telerik.com/blogs/integrating-google-places-autocomplete-api-react-app)”, _Progress Telerik Blogs_, Jun 29, 2022.

Kassym, Gapur. (2020) “[How to Use Google Place Autocomplete With React Without a Third-Party Library](https://betterprogramming.pub/the-best-practice-with-google-place-autocomplete-api-on-react-939211e8b4ce)”, _Better Programming_, Feb 3, 2020.

Kudamatsu, Masa. (2021) “[4 gotchas when setting up Google Maps API with Next.js and ESLint](https://medium.com/web-dev-survey-from-kyoto/3-gotchas-of-google-maps-api-when-used-with-next-js-and-eslint-dba627c9657d)”, _Web Dev Survey from Kyoto_, Feb 12, 2021.

MDN Contributors. (2023) “[Functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions)”, _MDN Web Docs_, Jan 31, 2023 (last updated).

Whitenton, Kathryn. (2014) “[3 Guidelines for Search Engine "No Results" Pages](https://www.nngroup.com/articles/search-no-results-serp/)”, _Nielsen Norman Group_, Jan 5, 2014.

Whiting, Jon. (2017) “[To ARIA! The Cause of, and Solution to, All Our Accessibility Problems](https://webaim.org/blog/aria-cause-solution/)”, _WebAIM_, May 17, 2017.