Skip to main content

Overview

The application integrates Google Maps for displaying property locations, interactive maps, and address autocomplete. The implementation uses the @googlemaps/js-api-loader package with Leaflet as a fallback option.

Installation

1

Install Dependencies

Install the required packages:
npm install @googlemaps/js-api-loader
npm install @types/google.maps
npm install leaflet @types/leaflet react-leaflet
2

Configure API Key

Add your Google Maps API key to environment variables:
VITE_GOOGLE_MAPS_API_KEY=your_api_key_here
Never commit your API key to version control. Always use environment variables and add .env files to .gitignore.

Google Maps Loader

The application uses a centralized loader utility to ensure the Google Maps API is loaded only once and shared across components.

Loader Implementation

src/lib/googleMaps.ts
let googlePromise: Promise<any> | null = null;

export const ensureGoogleMaps = (): Promise<any> => {
  if (googlePromise) return googlePromise;

  const apiKey = import.meta.env.VITE_GOOGLE_MAPS_API_KEY as string | undefined;
  if (!apiKey) {
    console.error("VITE_GOOGLE_MAPS_API_KEY is not set");
    return Promise.reject(new Error("Missing Google Maps API key"));
  }

  googlePromise = (async () => {
    const { Loader } = await import("@googlemaps/js-api-loader");
    const loader = new Loader({
      apiKey,
      version: "weekly",
      libraries: ["places"],
    });
    await loader.load();
    return (window as unknown as { google: any }).google;
  })();

  return googlePromise;
};
The loader uses a singleton pattern to prevent multiple API loads. The googlePromise is cached and reused across all components.

Key Features

  • Singleton Pattern: Ensures Google Maps API loads only once
  • Places Library: Includes the Places API for autocomplete functionality
  • Error Handling: Validates API key presence and handles load failures
  • Type Safety: TypeScript types for Google Maps objects

Property Map Component

The PropertyMap component displays a static map with a marker for property locations.

Basic Usage

import PropertyMap from "./components/PropertyMap";

function PropertyDetail() {
  return (
    <PropertyMap
      latitude={-34.603722}
      longitude={-58.381592}
      title="Beautiful Apartment"
      height="400px"
      zoom={15}
    />
  );
}

Component Props

PropTypeDefaultDescription
latitudenumberRequiredProperty latitude coordinate
longitudenumberRequiredProperty longitude coordinate
titlestring"Ubicación de la propiedad"Marker title (tooltip)
classNamestring""Additional CSS classes
heightstring"400px"Map container height
zoomnumber15Initial zoom level

Implementation Details

src/components/PropertyMap.tsx
const PropertyMap = ({
  latitude,
  longitude,
  title = "Ubicación de la propiedad",
  className = "",
  height = "400px",
  zoom = 15,
}: PropertyMapProps) => {
  const mapRef = useRef<HTMLDivElement>(null);
  const mapInstanceRef = useRef<GoogleMapInstance | null>(null);
  const markerRef = useRef<GoogleMarkerInstance | null>(null);

  useEffect(() => {
    if (!mapRef.current || !latitude || !longitude) return;

    const apiKey = import.meta.env.VITE_GOOGLE_MAPS_API_KEY;
    if (!apiKey) {
      console.error("VITE_GOOGLE_MAPS_API_KEY is not set");
      return;
    }

    let isCancelled = false;

    loadGoogleMapsApi(apiKey)
      .then((google) => {
        if (isCancelled || !mapRef.current) return;
        
        const map = new google.maps.Map(mapRef.current, {
          center: { lat: latitude, lng: longitude },
          zoom,
          mapTypeControl: false,
          streetViewControl: false,
          fullscreenControl: true,
          gestureHandling: "greedy",
        });
        mapInstanceRef.current = map;

        const marker = new google.maps.Marker({
          position: { lat: latitude, lng: longitude },
          map,
          title,
        });
        markerRef.current = marker;
      })
      .catch((err) => {
        console.error("Error loading Google Maps:", err);
      });

    return () => {
      isCancelled = true;
      if (markerRef.current) {
        markerRef.current.setMap(null);
        markerRef.current = null;
      }
      mapInstanceRef.current = null;
    };
  }, [latitude, longitude, title, zoom]);

  return (
    <div
      ref={mapRef}
      className={`rounded-lg overflow-hidden border border-gray-300 ${className}`}
      style={{ height }}
      tabIndex={0}
      aria-label={`Mapa mostrando la ubicación de ${title}`}
    />
  );
};
The component includes proper cleanup in the useEffect return function to prevent memory leaks when the component unmounts.

Draggable Property Map

For property creation and editing forms, use the DraggablePropertyMap component that allows users to adjust the marker position.

Usage

import { DraggablePropertyMap } from "./components/PropertyMap";
import { useState } from "react";

function PropertyForm() {
  const [latitude, setLatitude] = useState(-34.603722);
  const [longitude, setLongitude] = useState(-58.381592);

  const handleLocationChange = (lat: number, lng: number) => {
    setLatitude(lat);
    setLongitude(lng);
    console.log("New location:", lat, lng);
  };

  return (
    <DraggablePropertyMap
      latitude={latitude}
      longitude={longitude}
      onLocationChange={handleLocationChange}
      draggable={true}
      height="500px"
      zoom={16}
    />
  );
}

Additional Props

PropTypeDefaultDescription
onLocationChange(lat: number, lng: number) => void-Callback when marker is dragged
draggablebooleantrueEnable/disable marker dragging

Drag Event Implementation

const marker = new google.maps.Marker({
  position: { lat: latitude, lng: longitude },
  map,
  title,
  draggable, // Enable dragging
});

// Add drag event listener
if (onLocationChange && draggable) {
  marker.addListener("dragend", () => {
    const position = marker.getPosition();
    if (position) {
      onLocationChange(position.lat(), position.lng());
    }
  });
}

Map Configuration Options

Customize map behavior with Google Maps options:
const map = new google.maps.Map(mapRef.current, {
  center: { lat: latitude, lng: longitude },
  zoom: 15,
  // Disable default controls
  mapTypeControl: false,
  streetViewControl: false,
  // Enable fullscreen
  fullscreenControl: true,
  // Smooth gesture handling
  gestureHandling: "greedy",
  // Custom styles
  styles: [
    {
      featureType: "poi",
      elementType: "labels",
      stylers: [{ visibility: "off" }]
    }
  ]
});

Common Configuration Options

OptionTypeDescription
zoomnumberInitial zoom level (0-22)
center{ lat: number, lng: number }Map center coordinates
mapTypeControlbooleanShow map type selector
streetViewControlbooleanShow Street View control
fullscreenControlbooleanShow fullscreen button
gestureHandling"cooperative" | "greedy" | "none"Touch/scroll behavior
stylesMapTypeStyle[]Custom map styling

Address Autocomplete

Implement address autocomplete using the Places API:
import { useEffect, useRef } from "react";
import { ensureGoogleMaps } from "../lib/googleMaps";

function AddressAutocomplete({ onPlaceSelect }) {
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    if (!inputRef.current) return;

    ensureGoogleMaps().then((google) => {
      const autocomplete = new google.maps.places.Autocomplete(
        inputRef.current,
        {
          types: ["address"],
          componentRestrictions: { country: "ar" }, // Argentina only
        }
      );

      autocomplete.addListener("place_changed", () => {
        const place = autocomplete.getPlace();
        
        if (place.geometry?.location) {
          onPlaceSelect({
            address: place.formatted_address,
            latitude: place.geometry.location.lat(),
            longitude: place.geometry.location.lng(),
          });
        }
      });
    });
  }, [onPlaceSelect]);

  return (
    <input
      ref={inputRef}
      type="text"
      placeholder="Search address..."
      className="form-input"
    />
  );
}

Autocomplete Options

const autocomplete = new google.maps.places.Autocomplete(input, {
  types: ["address"],           // Filter to addresses only
  componentRestrictions: { 
    country: "ar"                // Restrict to Argentina
  },
  fields: [                      // Limit returned fields
    "formatted_address",
    "geometry",
    "name"
  ]
});

Leaflet Integration

For fallback scenarios or offline capabilities, the application includes Leaflet:
import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
import "leaflet/dist/leaflet.css";

function LeafletMap({ latitude, longitude }) {
  return (
    <MapContainer
      center={[latitude, longitude]}
      zoom={15}
      style={{ height: "400px", width: "100%" }}
    >
      <TileLayer
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
        attribution='&copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a>'
      />
      <Marker position={[latitude, longitude]}>
        <Popup>Property Location</Popup>
      </Marker>
    </MapContainer>
  );
}

Error Handling

Handle API load failures gracefully:
const [mapError, setMapError] = useState<string | null>(null);

useEffect(() => {
  loadGoogleMapsApi(apiKey)
    .then((google) => {
      // Initialize map
    })
    .catch((err) => {
      console.error("Error loading Google Maps:", err);
      setMapError("Failed to load map. Please refresh the page.");
    });
}, []);

if (mapError) {
  return (
    <div className="map-error">
      <p>{mapError}</p>
      <button onClick={() => window.location.reload()}>
        Retry
      </button>
    </div>
  );
}

Performance Optimization

1

Lazy Loading

Load Google Maps API only when needed:
const [shouldLoadMap, setShouldLoadMap] = useState(false);

// Load map when component is visible
useEffect(() => {
  const observer = new IntersectionObserver((entries) => {
    if (entries[0].isIntersecting) {
      setShouldLoadMap(true);
    }
  });
  observer.observe(mapContainerRef.current);
}, []);
2

Debounce Updates

Debounce location updates in draggable maps:
const debouncedLocationChange = useMemo(
  () => debounce((lat, lng) => {
    onLocationChange(lat, lng);
  }, 300),
  [onLocationChange]
);
3

Cleanup Resources

Always clean up map instances and event listeners:
return () => {
  if (markerRef.current) {
    markerRef.current.setMap(null);
  }
  if (autocompleteRef.current) {
    google.maps.event.clearInstanceListeners(autocompleteRef.current);
  }
};

Best Practices

  • Load the API only once using the singleton pattern
  • Always validate API key presence before loading
  • Implement proper error handling and fallbacks
  • Clean up resources in useEffect return functions
  • Use gestureHandling: "greedy" for better mobile UX
  • Limit Places API fields to reduce costs
  • Consider lazy loading maps for performance

Troubleshooting

API Key Issues

If maps fail to load:
  1. Verify API key is set in environment variables
  2. Check API key restrictions in Google Cloud Console
  3. Ensure Maps JavaScript API is enabled
  4. Verify billing is set up (required for Google Maps)

TypeScript Errors

Install type definitions:
npm install --save-dev @types/google.maps

Marker Not Visible

Ensure coordinates are valid:
if (!latitude || !longitude) {
  return <div>Invalid coordinates</div>;
}

Build docs developers (and LLMs) love