DEV Community

Mai G.
Mai G.

Posted on

React: How to Implement Search Location Within Radius Feature

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;
}

Enter fullscreen mode Exit fullscreen mode

Google Maps Geocoding API

Head to this link and obtain your own API key.

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([]);

Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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);
};

Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)