OWolf

AboutBlogProjects
©2025 OWolf.com

Privacy

Contact

Web Development

Google Map Viewer in React

May 19, 2024

O Wolfson

In this article, we'll explore how to create a Google Map Viewer using React and Next.js. This app allows users to input a Google Maps link, which then displays the corresponding location on a map. You can check out the demo here.

Get the source code on GitHub.

Project Structure

Our project consists of a main page (app/page.tsx) and four components located in the components/maps folder.

plaintext
app/
  page.tsx
components/
  maps/
    google-map.tsx
    map-component.tsx
    map-provider.tsx
    map-utils.ts

Let's walk through these components and their functionalities in a logical order.

1. map-utils.ts

This utility module handles URL expansion and extraction of latitude and longitude coordinates from Google Maps links. It contains two main functions:

  • expandUrl: Expands shortened URLs to their full version.
  • extractLatLong: Extracts latitude and longitude coordinates from either DMS (Degrees, Minutes, Seconds) or decimal formats.
tsx
"use server";

import axios from "axios";

export const expandUrl = async (shortUrl: string): Promise<string | null> => {
  try {
    const response = await axios.get(shortUrl, {
      maxRedirects: 0,
      validateStatus: (status) => status === 302, // Handle redirect
    });
    return response.headers.location; // This contains the expanded URL
  } catch (error) {
    console.error("Error expanding URL:", error);
    return null;
  }
};

const dmsToDecimal = (
  degrees: number,
  minutes: number,
  seconds: number,
  direction: string
): number => {
  const decimal = degrees + minutes / 60 + seconds / 3600;
  return direction === "S" || direction === "W" ? -decimal : decimal;
};

export const extractLatLong = (
  url: string
): { lat: number; lng: number } | null => {
  // Decode the URL to handle URL-encoded characters
  const decodedUrl = decodeURIComponent(url);
  // console.log("Decoded URL:", decodedUrl);

  // Regex to match DMS coordinates
  const dmsPattern = /(\d{1,3})°(\d{1,2})'(\d{1,2}(?:\.\d+)?)"?([NSEW])/g;
  // Regex to match decimal coordinates
  const decimalPattern = /@(-?\d+\.\d+),(-?\d+\.\d+)/;
  // Regex to match accurate 3d/4d coordinates
  const accurateDecimalPattern = /3d(-?\d+\.\d+)!4d(-?\d+\.\d+)/;

  let match;
  let lat, lng;

  // Try to extract accurate 3d/4d coordinates
  match = accurateDecimalPattern.exec(decodedUrl);
  if (match) {
    return {
      lat: parseFloat(match[1]),
      lng: parseFloat(match[2]),
    };
  }

  // Try to extract DMS coordinates for latitude and longitude
  const dmsMatches = decodedUrl.match(dmsPattern);
  if (dmsMatches && dmsMatches.length >= 2) {
    const latMatch = dmsMatches[0].match(
      /(\d{1,3})°(\d{1,2})'(\d{1,2}(?:\.\d+)?)"?([NSEW])/
    );
    const lngMatch = dmsMatches[1].match(
      /(\d{1,3})°(\d{1,2})'(\d{1,2}(?:\.\d+)?)"?([NSEW])/
    );

    if (latMatch && lngMatch) {
      lat = dmsToDecimal(
        parseInt(latMatch[1]),
        parseInt(latMatch[2]),
        parseFloat(latMatch[3]),
        latMatch[4]
      );
      lng = dmsToDecimal(
        parseInt(lngMatch[1]),
        parseInt(lngMatch[2]),
        parseFloat(lngMatch[3]),
        lngMatch[4]
      );

      return { lat, lng };
    }
  }

  // Fallback to extract decimal coordinates if accurate coordinates are not found
  match = decimalPattern.exec(decodedUrl);
  if (match) {
    return {
      lat: parseFloat(match[1]),
      lng: parseFloat(match[2]),
    };
  }

  return null;
};

2. map-provider.tsx

The MapProvider component loads the Google Maps JavaScript API and provides it to the rest of the app. This component uses the useJsApiLoader hook from the @react-google-maps/api library to load the API asynchronously.

tsx
"use client";

import { Libraries, useJsApiLoader } from "@react-google-maps/api";
import { ReactNode } from "react";

const libraries = ["places", "drawing", "geometry"];

export function MapProvider({ children }: { children: ReactNode }) {
  const { isLoaded: scriptLoaded, loadError } = useJsApiLoader({
    googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API as string,
    libraries: libraries as Libraries,
  });

  if (loadError) return <p>Encountered error while loading google maps</p>;

  if (!scriptLoaded) return <p>Map Script is loading ...</p>;

  return children;
}

3. map-component.tsx

The MapComponent is the core component that renders the map, marker, and info window. It also provides an input form for users to enter a Google Maps link, title, and address.

tsx
"use client";

import React, { useState, useEffect } from "react";
import { GoogleMap, Marker, InfoWindow } from "@react-google-maps/api";
import Link from "next/link";
import { expandUrl, extractLatLong } from "./map-utils";

const defaultMapOptions = {
  zoomControl: true,
  tilt: 0,
  gestureHandling: "auto",
  mapTypeId: "roadmap",
};

const defaultMapZoom = 18;

const info = {
  title: "The Empire State Building",
  address: "20 W 34th St., New York, NY 10001, United States",
  link: "https://maps.app.goo.gl/qz2zoCrJpmjH7Pmk7",
};

export interface MapCenter {
  lat: number;
  lng: number;
}

export const defaultMapContainerStyle = {
  height: "300px",
  borderRadius: "15px 15px 15px 15px",
};

const MapComponent: React.FC = () => {
  const [isOpen, setIsOpen] = useState(false);
  const [mapCenter, setMapCenter] = useState<>();
   [mapInfo, setMapInfo] = (info);
   [tempMapInfo, setTempMapInfo] = (info);
   [isLoading, setIsLoading] = ();
   [isSuccess, setIsSuccess] = ();

  ( {
      =  () => {
       expandedUrl =  (mapInfo.);
       (expandedUrl) {
         coords =  (expandedUrl);
         (coords) {
          (coords);
        }
      }
    };

     (mapInfo.) {
      ();
    }
  }, [mapInfo.]);

    =  () => {
    e.();
    ();
    ();
    (tempMapInfo);
    ();
    ();
    ( (), );
  };

   (
    
      
    </div>
  );
};

 {  };

4. google-map.tsx

The GoogleMap component serves as a container for the MapProvider and MapComponent. It ensures that the Google Maps API is loaded before rendering the map.

tsx
import React from "react";
import { MapProvider } from "@/components/maps/map-provider";
import { MapComponent } from "@/components/maps/map-component";

function GoogleMap() {
  return (
    <div>
      <MapProvider>
        <MapComponent />
      </MapProvider>
    </div>
  );
}

export default GoogleMap;

Main Page (app/page.tsx)

Finally, the main page of our Next.js app (app/page.tsx) imports and uses the GoogleMap component.

tsx
import GoogleMap from "@/components/maps/google-map";

export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <div className="w-full">
        <GoogleMap />
      </div>
    </main>
  );
}

Conclusion

By structuring our components logically and leveraging the power of React and Next.js, we have created a robust Google Map Viewer application. The MapComponent handles user input and displays the map, while MapProvider ensures the Google Maps API is loaded correctly. The map-utils.ts module provides essential utility functions for URL and coordinate handling.

Feel free to explore the demo and try out different Google Maps links. Happy coding!


▊
MapCenter
const
useState
const
useState
const
useState
false
const
useState
false
useEffect
() =>
const
fetchCoordinates
async
const
await
expandUrl
link
if
const
await
extractLatLong
if
setMapCenter
if
link
fetchCoordinates
link
const
handleSubmit
async
e: React.FormEvent<HTMLFormElement>
preventDefault
setIsLoading
true
setIsSuccess
false
setMapInfo
setIsLoading
false
setIsSuccess
true
setTimeout
() =>
setIsSuccess
false
1500
return
<div className="flex flex-col gap-4"> <GoogleMap mapContainerStyle={defaultMapContainerStyle} center={mapCenter} zoom={defaultMapZoom} options={defaultMapOptions} > {mapCenter && ( <> <Marker position={mapCenter} onClick={() => setIsOpen(true)} /> {isOpen && ( <InfoWindow position={mapCenter} onCloseClick={() => setIsOpen(false)} > <div> <div className="text-black flex flex-col gap-1"> <div className="font-semibold text-lg -mb-1"> {mapInfo.title} </div> <div>{mapInfo.address}</div> <Link target="_blank" href={mapInfo.link} className="font-semibold text-blue-500 hover:underline" > View on Google Maps </Link> </div> </div> </InfoWindow> )} </> )} </GoogleMap>
<form onSubmit={handleSubmit} className="flex flex-col gap-2"> <div> <label htmlFor="mapLink" className="block text-sm p-1"> Map Link </label> <input id="mapLink" type="text" value={tempMapInfo.link} onChange={(e) => setTempMapInfo({ ...tempMapInfo, link: e.target.value }) } placeholder="Enter the map link" className="border border-gray-300 rounded-md p-2 w-full text-black" /> </div> <div> <label htmlFor="mapTitle" className="block text-sm p-1"> Map Title </label> <input id="mapTitle" type="text" value={tempMapInfo.title} onChange={(e) => setTempMapInfo({ ...tempMapInfo, title: e.target.value }) } placeholder="Enter the map title" className="border border-gray-300 rounded-md p-2 w-full text-black" /> </div> <div> <label htmlFor="mapAddress" className="block text-sm p-1"> Map Address </label> <input id="mapAddress" type="text" value={tempMapInfo.address} onChange={(e) => setTempMapInfo({ ...tempMapInfo, address: e.target.value }) } placeholder="Enter the map address" className="border border-gray-300 rounded-md p-2 w-full text-black" /> </div> <button type="submit" className="mt-4 bg-blue-500 text-white p-2 rounded-md active:bg-blue-400" disabled={isLoading} > {isLoading ? "Submitting..." : "Submit"} </button> {isSuccess && ( <div className="text-green-500 mt-2"> Map information updated successfully! </div> )} </form>
export
MapComponent