React: How to Implement Search Location Within Radius Feature
Tags: react, geocode, webdev
Published: 2023-07-21 21:50:11

In phase 4 of my Software Engineering Bootcamp at Flatiron, along with 2 other exceptional classmates, we built a full stack app using React front-end and Flask back-end. One of the features that we got questions about during our presentation was how we implemented the search locations within a radius feature, so I figured I'd write my a blog on the steps I took to accomplish this feature.

## Haversine Formula
In a nutshell,  the **haversine formula** determines the great-circle distance between two points given their longitudes and latitudes. As scary as it might sound, we don't have to reinvent the wheel. You first would need to create an element to store this function which takes in 4 variables.

```
export default function calculateDistance(lat1, lon1, lat2, lon2) {
  const R = 6371;
  const phi1 = (lat1 * Math.PI) / 180;
  const phi2 = (lat2 * Math.PI) / 180;
  const deltaPhi = ((lat2 - lat1) * Math.PI) / 180;
  const deltaLambda = ((lon2 - lon1) * Math.PI) / 180;

  const a =
    Math.sin(deltaPhi / 2) * Math.sin(deltaPhi / 2) +
    Math.cos(phi1) *
      Math.cos(phi2) *
      Math.sin(deltaLambda / 2) *
      Math.sin(deltaLambda / 2);
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

  const distance = R * c;
  return distance;
}

```

## Google Maps Geocoding API
Head to this [link](https://developers.google.com/maps/documentation/geocoding/intro) and obtain your own API key.

## [React Geocode](https://www.npmjs.com/package/react-geocode)
Install **React Geocode** `npm i react-geocode` in order to convert the addresses in your app to longitude and latitude. This process is called _reverse geocoding_. Additionally, you will also need to build a form to take in user input, and store that using **useState**. The component where you implement this feature will look something like this:

```
import Search from "./Search"; # my search form
import calculateDistance from "path";
import Geocode from "react-geocode";
Geocode.setApiKey("YOUR-API-KEY");
import { useState, useEffect } from "react";

export default function YourFunction({searchTerm, setSearchTerm, array}) {
  const [userLocation, setUserLocation] = useState({ lat: 0, lon: 0 }); #store user's input in lat and lon
  const [distanceRadius, setDistanceRadius] = useState(10);
  const [filteredArray, setFilteredArray] = useState([]);

```

From here we will need to use the **useEffect** hook to implement the logic for filtering and sorting your array based on the user's location and the distance radius specified. 

```
  useEffect(() => {
    if (!searchTerm) {
      setFilteredArray(array);
      return;
    }
    Geocode.fromAddress(searchTerm).then(
      (response) => {
        const { lat, lng } = response.results[0].geometry.location;
        setUserLocation({ lat: lat, lon: lng });

        const updatedArray = array.map((item) => ({
          ...item,
          distance: calculateDistance(
            userLocation.lat,
            userLocation.lon,
            item.latitude,
            item.longitude
          ),
        }));

        const filteredArray = updatedArray.filter((item) => item.distance <= distanceRadius)
        setFilteredArray(filteredArray);
      },
      (error) => {
        console.error(error);
      }
    );
  }, [searchTerm, array, distanceRadius]);
```
You may notice that this **useEffect** hook has a few dependencies because we want the useEffect to fire every time there's a change in the **searchTerm**, the **array** itself as well as the **distanceRadius** (which is chosen by the user via the Search form).

Within the **useEffect**, there is an asynchronous operation: the **Geocode.fromAddress** function call. This function is used to convert the user's search term (address) into latitude and longitude coordinates. Since this is an asynchronous operation, it might not have the results immediately. By using **useEffect**, we ensure that this operation will be executed after the initial render and after every subsequent update, depending on the specified dependencies.

The **useEffect** hook also helps in avoiding infinite loops caused by frequent updates. Without **useEffect**, if we directly put the filtering and sorting logic in the component body, it could lead to a continuous update loop, as the state changes would trigger re-rendering and re-calculating distances over and over again. **useEffect** ensures that the filtering and sorting logic is executed at the right times without causing infinite re-renders.

Now that you've got your **filteredArray**, you just have to map through it the way you want it to display. 

Well... that was the best case scenario if your array already contains longitude and latitude. In our case, I had to convert addresses in each item to longitude and latitude, store all that along with the original data in a new array and pass that array down to the component above instead of the original array that we fetch. If you're in the same boat, follow the steps below.

Inside of my App.js component, I added the following above my App function:

```
const fetchItemLocation = async (item) => {
  const { street_address, city, state, country } = item;
  const itemAddress = `${street_address}, ${city}, ${state}, ${country}`;

  try {
    const response = await Geocode.fromAddress(itemAddress);
    const { lat, lng } = response.results[0].geometry.location;
    return { ...item, latitude: lat, longitude: lng };
  } catch (error) {
    console.error(error);
    return null;
  }
};
const calculateLatLongForArray = async (array) => {
  const updatedArray = await Promise.all(array.map(fetchItemLocation));
  return updatedArray.filter((item) => item !== null);
};

```

## fetchItemLocation function:
This function takes a single item object as its parameter and aims to fetch its latitude and longitude coordinates based on its address information. 
- We extract the address details (street_address, city, state, and country) from the item object and form a full address string
- We then reverse geocode the address using **Geocode.fromAddress**. If the geocoding is successful, it extracts the latitude (lat) and longitude (lng) from the response and returns a new object that includes the item's original data along with the calculated latitude and longitude. Otherwise, if will catch error and return **null**.

## calculateLatLongForArray function:
This function takes an array of item objects as its parameter and aims to calculate latitude and longitude coordinates for each item in the array using the **fetchItemLocation** function. We use **Promise.all** to handle multiple asynchronous calls concurrently on the **array.map(fetchItemLocation)** expression, which iterates through each item in the array and fetches its latitude and longitude using **fetchItemLocation**. This creates an array of promises that will resolve to an array of updated item objects (including latitude and longitude) or null if an error occurred during geocoding.

When all the promises are resolved (or rejected), **Promise.all** returns the resulting array of updated item objects.
The function then filters out any null values from the array using `.filter((item) => item !== null)`, removing any items where geocoding failed.

Finally, the **calculateLatLongForArray** function returns an array of updated item objects, each containing the original item data along with the latitude and longitude information obtained from geocoding. 

Inside of my App function, I had a state to store **updatedArray** `const [updatedArray, setUpdatedArray] = useState([]);` and a loading state to store the loading status of the original array `  const [isLoading, setIsLoading] = useState(false);`

After I fetch the original array using **useEffect**, it goes through another **useEffect** hook:

```
 useEffect(() => {
    if (!isLoading) {
      calculateLatLongForArray(array).then((updatedArray) => {
        setUpdatedArray(updatedArray);
      });
    }
  }, [isLoading, array]);
```

To make sure the second **useEffect** only runs after the array has fully loaded on the front-end, the **isLoading** state is necessary. We also added **isLoading** and array as the dependencies to make sure this **useEffect** hook runs again whenever those two change. 

Now your **updatedArray** is ready for the **calculatingDistance** function! 

Happy coding!