How to Get user location & address autocompletion in React

demawo

Prince De Mawo

Posted on June 13, 2023

How to Get user location & address autocompletion in React

One of the key aspects of creating web apps is ensuring a great user experience. However, manually filling out forms for example on the checkout page of an ecommerce website, including entering delivery addresses, can be tedious. In this tutorial, we will explore a solution that allows users to autocomplete their delivery addresses by typing a few characters.

Demo App: App Demo

Prerequisites:

. Basic knowledge of JavaScript
. A Mapbox account
. Basic knowledge of React
. Familiarity with Tailwind CSS
. Familiarity with Next.js App Router (easy to follow through, don't worry)

Let's dive into the implementation:

Open your terminal and navigate to the directory where you want to create the tutorial folder. For beginners, you can use the command
cd Desktop

Clone the repository: git clone https://github.com/de-mawo/geocoding.git

Once the files are downloaded, navigate to the geocoding directory: cd geocoding

Use the git checkout command to switch to the appropriate branch based on your preference:

For TypeScript users: git checkout ts-starter
For JavaScript users: git checkout js-starter

If you are using VSCode, you can simply type code . in the terminal to open the current folder in the VSCode IDE.

Here is the folder structure you should see if you chose ts-starter branch like me:

Folder structure
If you chose the js-starter repository, your file extensions will end with .js instead of .tsx.

In your terminal, run yarn to install all dependencies. Then, run yarn dev to start the development server. Open your browser and go to localhost:3000 to see a basic navbar.

If you haven't already, please create a Mapbox account by visiting https://account.mapbox.com/auth/signup/.

After creating the account, proceed to the Dashboard and generate an access token. This token will be used in our endpoints, as demonstrated below.

Once the token is generated, you can view its value and copy it. You also have the option to edit the token. To do so, click on "edit" and add http://localhost:3000/ as one of the URLs under the "URLs" section. When you deploy your app, remember to add another production URL.

Now that you have an access token, create a file named .env at the root level of your folder and include your token in it, like this:

REACT_APP_MAPBOX_TOKEN=pk.ejfadhssxxxxxxxxxxxxxxx

Now let's start coding by focusing on the LocationSearchForm component. Edit the component and update it to the following code.

"use client"
import { ChangeEvent, useEffect, useState } from "react";
import { HiMapPin, HiOutlinePencil } from "react-icons/hi2";
import { toast } from "react-hot-toast";

const LocationSearchForm = () => {
  const [isEditing, setIsEditing] = useState<boolean>(false);

  const [location, setLocation] = useState<{
    latitude: number;
    longitude: number;
  } | null>(null);

  const [query, setQuery] = useState("");
  const [suggestions, setSuggestions] = useState<Array<{ place_name: string }>>(
    []
  );

  // const checknavigator = navigator.permissions
  // console.log(checknavigator);

  /* Reverse geocoding */
  /* Get User Location on page load and if granted permission by User */
  useEffect(() => {
    const askForLocationPermission = () => {
      navigator.geolocation.getCurrentPosition(
        (position) => {
          const { latitude, longitude } = position.coords;
          setLocation((prevLocation) => ({
            ...prevLocation,
            latitude,
            longitude,
          }));
        },
        (error) => {
          // Handle location access denied or error
          toast.error("Error getting location:");
          console.log(error);
        }
      );
    };

    // Check if geolocation is supported by the browser
    if ("geolocation" in navigator) {
      // Ask for permission
      navigator.permissions
        .query({ name: "geolocation" })
        .then((result) => {
          if (result.state === "granted") {
            // Permission already granted
            askForLocationPermission();
          } else if (result.state === "prompt") {
            // Permission not yet granted, ask the user
            askForLocationPermission();
          } else if (result.state === "denied") {
            // Permission denied, handle accordingly
            toast.error("Location access denied by the user."{ duration: 1000});
          }
        })
        .catch((error) => {
          // Handle error
          console.error("Error checking location permission:", error);
        });
    } else {
      // Geolocation is not supported
      toast.error("Geolocation is not supported by this browser."{ duration: 1000});
    }
  }, []);

  // Set location and save on local storage based on User granting location permission
  useEffect(() => {
    if (location) {
      // search for place name using mapbox API    
      const endpoint = `https://api.mapbox.com/geocoding/v5/mapbox.places/${location.longitude},${location.latitude}.json?proximity=-33.9249,18.4241&country=ZA&access_token=${process.env.REACT_APP_MAPBOX_TOKEN}`;

      fetch(endpoint)
        .then((response) => response.json())
        .then((data) => {
          const place = data.features[0].place_name;
          localStorage.setItem("delivery_address", place);
          setQuery(place);
        });
    }
  }, [location]);

  /*Forward geocoding */
  /* Look for location name being queried by user */
  const handleChange = async (event: ChangeEvent<HTMLInputElement>) => {
    try {
      setQuery(event.target.value);
      const endpoint = `https://api.mapbox.com/geocoding/v5/mapbox.places/${event.target.value}.json?proximity=-33.9249,18.4241&country=ZA&access_token=${process.env.REACT_APP_MAPBOX_TOKEN}&autocomplete=true`;

      const response = await fetch(endpoint);
      const results = await response.json();
      // console.log(results);
      setSuggestions(results?.features);
      //  console.log(suggestions);
    } catch (error: any) {
      console.log("Error fetching data: " + error.message);
    }
  };

  /* Display location selected by User  */
  const handleSelectAddress = (selectedAddress: string) => {
    localStorage.setItem("delivery_address", selectedAddress);
    setQuery(selectedAddress);
    setSuggestions([]);
    setIsEditing(false);
  };

  return (
    <div className="mx-8 md:mx-12 mt-12">
      <form className="max-w-6xl mx-auto ">
        <div className="relative">
          {isEditing ? (
            <>
              <div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
                <HiMapPin
                  aria-hidden="true"
                  className="w-5 h-5 text-gray-700 "
                />
              </div>
              <input
                type="search"
                className="block w-full p-4 pl-10 text-sm text-gray-900 rounded-lg bg-gray-200 outline-none"
                placeholder="Enter your address"
                value={query}
                onChange={handleChange}
              />
            </>
          ) : (
            <div className="flex flex-col " onClick={() => setIsEditing(true)}>
              <p className="">{query}</p>
              <button className="px-4 py-1 mt-2 w-24  inline-flex items-center text-green-600 bg-green-200 hover:bg-green-300 border border-green-500 focus-visible:ring-2 rounded-full  ">
                <HiOutlinePencil
                  className="mr-1 -ml-1 w-4 h-4"
                  fill="currentColor"
                />
                Edit
              </button>
            </div>
          )}
          {suggestions?.length > 0 && (
            <div className="absolute bg-gray-100 w-full shadow-sm">
              {suggestions.map((suggestion, index) => (
                <div
                  key={index}
                  className="flex items-center justify-between w-full p-1 cursor-pointer hover:bg-gray-200"
                  onClick={() => handleSelectAddress(suggestion.place_name)}
                >
                  {suggestion.place_name}
                </div>
              ))}
            </div>
          )}
        </div>
      </form>
    </div>
  );
};

export default LocationSearchForm;
Enter fullscreen mode Exit fullscreen mode

This code snippet is a React component for a location search form. It provides an input field where users can search for a location. The form utilizes the Mapbox Geocoding API to provide autocomplete suggestions based on the user's input.

Our endpoint supports both required and optional parameters. You can find detailed information about these parameters at mapbox signup.

For optional parameters, I have included "proximity" to specify the default search area. This allows you to search within a specific proximity. Additionally, I have localized the country for my searches. You can refer to the country codes at country codes for more information on selecting the appropriate country code.

The component uses the following state variables:

isEditing: A boolean variable to track whether the user is editing the location input or not.
location: A nullable object that holds the latitude and longitude of the user's current location. This information is retrieved using the Geolocation API.
query: The current value of the location input field.
suggestions: An array of location suggestions based on the user's input.
The component consists of two useEffect hooks:

The first useEffect hook runs once when the component mounts and checks for Geolocation API support. If supported, it asks for the user's location permission. If granted, it sets the location state with the latitude and longitude of the user's current location. If the permission is denied, an error message is shown.

The second useEffect hook runs whenever the location state changes. It makes a request to the Mapbox Geocoding API to retrieve the place name of the user's current location. The place name is then stored in local storage and set as the initial value of the location input field.

The component also includes a handleChange function that is triggered when the user types in the location input field. This function sends a request to the Mapbox Geocoding API with the user's input to fetch autocomplete suggestions. The suggestions are stored in the suggestions state.

When the user selects a suggested address, the handleSelectAddress function is called. It stores the selected address in local storage, sets it as the value of the location input field, clears the suggestions, and sets isEditing to false.

The JSX code renders the location search form, including the input field, suggestions dropdown (if there are suggestions).

Overall, this component provides a user-friendly location search form with autocomplete suggestions and the ability to detect the user's current location.

Now let's also update our LocationBtn component to include the following complete code

'use client'

import { Fragment, useState } from "react";
import {  HiMapPin } from "react-icons/hi2";
import { FaChevronRight } from "react-icons/fa";
import { Dialog, Transition } from "@headlessui/react";
import LocationSearchForm from "./LocationSearchForm";

const LocationBtn = () => {
  const [isOpen, setIsOpen] = useState(false);
  const [showChange, setShowChange] = useState(false);
  const deliveryAddress  =   typeof window !== "undefined" && localStorage?.getItem("delivery_address");


  const openModal = () => setIsOpen(true);
  const closeModal = () => {
    setShowChange(false);
    setIsOpen(false);
  };

  return (
    <>
      <button
        onClick={openModal}
        className={`flex items-center px-4 py-2 bg-slate-200 rounded-full md:max-w-sm  md:rounded-lg`}
      >
        {" "}
        <HiMapPin className="shrink-0 text-green-600" />{" "}
        <span className="h-2 w-2 mx-2 bg-gray-600 shrink-0 rounded-full hidden md:block ">
          {" "}
        </span>{" "}
        <span
          className={
            "truncate max-w-[8rem]  text-sm text-gray-500 md:max-w-[12rem]"
          }
        >
          {deliveryAddress  ? deliveryAddress  : "Enter Delivery Address"}
        </span>
        <FaChevronRight className=" shrink-0 text-green-600" />
      </button>

      <Transition appear show={isOpen} as={Fragment}>
        <Dialog as="div" className="relative z-10" onClose={closeModal}>
          <Transition.Child
            as={Fragment}
            enter="ease-out duration-300"
            enterFrom="opacity-0"
            enterTo="opacity-100"
            leave="ease-in duration-200"
            leaveFrom="opacity-100"
            leaveTo="opacity-0"
          >
            <div className="fixed inset-0 bg-black bg-opacity-25" />
          </Transition.Child>

          <div className="fixed inset-0 overflow-y-auto">
            <div className="flex min-h-full items-center justify-center p-4 text-center">
              <Transition.Child
                as={Fragment}
                enter="ease-out duration-300"
                enterFrom="opacity-0 scale-95"
                enterTo="opacity-100 scale-100"
                leave="ease-in duration-200"
                leaveFrom="opacity-100 scale-100"
                leaveTo="opacity-0 scale-95"
              >
                <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
                  <Dialog.Title
                    as="h3"
                    className="text-lg font-medium leading-6 text-gray-900"
                  >
                    Delivery Address
                  </Dialog.Title>

                  {showChange ? (
                    <div className="mt-2">
                      <LocationSearchForm />
                    </div>
                  ) : (
                    <div className="flex items-center mt-8 justify-between">
                      <div>
                        <p className="truncate max-w-[10rem] md:max-w-xs">
                          {deliveryAddress 
                            ? deliveryAddress 
                            : "Click change..."}
                        </p>{" "}
                      </div>

                      <div>
                        {" "}
                        <button
                          className="px-4 py-1 text-slate-600 bg-green-100 hover:bg-green-200 border border-green-500  rounded-full"
                          onClick={() => setShowChange(true)}
                        >
                          Change
                        </button>
                      </div>
                    </div>
                  )}

                  <div className="mt-12 mx-12">
                    <button
                      type="submit"
                      className="px-4 py-1 w-full text-white bg-green-600 hover:bg-green-500 border border-green-600  rounded-full"
                      onClick={closeModal}
                    >
                      Done
                    </button>
                  </div>
                </Dialog.Panel>
              </Transition.Child>
            </div>
          </div>
        </Dialog>
      </Transition>
    </>
  );
};

export default LocationBtn;
Enter fullscreen mode Exit fullscreen mode

The LocationBtn component is a button that displays the delivery address. It includes a modal dialog that allows the user to change the address. The address is retrieved from the browser's local storage and displayed in the button. If no address is available, a placeholder text is shown. Clicking on the button opens the modal dialog where the user can either view the current address or change it using a LocationSearchForm component. After making any changes, the user can click the "Done" button to close the dialog.

Great! Now let's move on to testing. If you click on the button, you should see a map pin in your browser's address bar.

localhost demo
Additionally, you should be able to edit the address and select an auto-completed option.

Please note that the location permission popup may not appear on localhost. However, once you deploy your app and update the URL permissions in your Mapbox access tokens, everything should work fine.

Production image

By clicking on the cart, you will be redirected to the /cart route where you can find the LocationBtn once again. This allows you to easily change the address displayed. This user experience eliminates the need for manual address typing, providing convenience for users.

Thank you for reading through. Watch a tutorial video

💖 💪 🙅 🚩
demawo
Prince De Mawo

Posted on June 13, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related