CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-react-google-maps--api

React.js Google Maps API integration with components and hooks for seamless Google Maps functionality

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

places.mddocs/

Places Integration

Components for integrating Google Places API functionality including autocomplete, place search capabilities, and location-based services with comprehensive place data access.

Capabilities

Autocomplete Component

Provides place autocomplete functionality for input fields with extensive filtering and customization options.

/**
 * Provides place autocomplete functionality for input fields
 * Enhances text inputs with intelligent place suggestions and validation
 */
interface AutocompleteProps {
  children: React.ReactNode; // Required - must contain exactly one input element
  bounds?: google.maps.LatLngBounds | google.maps.LatLngBoundsLiteral;
  restrictions?: google.maps.places.ComponentRestrictions;
  fields?: string[];
  options?: google.maps.places.AutocompleteOptions;
  types?: string[];
  
  // Event handlers
  onPlaceChanged?: () => void;
  
  // Lifecycle events
  onLoad?: (autocomplete: google.maps.places.Autocomplete) => void;
  onUnmount?: (autocomplete: google.maps.places.Autocomplete) => void;
}

interface google.maps.places.AutocompleteOptions {
  bounds?: google.maps.LatLngBounds | google.maps.LatLngBoundsLiteral;
  componentRestrictions?: google.maps.places.ComponentRestrictions;
  fields?: string[];
  placeIdOnly?: boolean;
  strictBounds?: boolean;
  types?: string[];
}

interface google.maps.places.ComponentRestrictions {
  country?: string | string[];
}

function Autocomplete(props: AutocompleteProps): JSX.Element;

Usage Examples:

import React, { useState, useRef } from 'react';
import { GoogleMap, LoadScript, Autocomplete } from '@react-google-maps/api';

// Basic autocomplete
function BasicAutocomplete() {
  const [selectedPlace, setSelectedPlace] = useState<google.maps.places.PlaceResult | null>(null);
  const autocompleteRef = useRef<google.maps.places.Autocomplete | null>(null);
  
  const onLoad = (autocomplete: google.maps.places.Autocomplete) => {
    autocompleteRef.current = autocomplete;
  };
  
  const onPlaceChanged = () => {
    if (autocompleteRef.current) {
      const place = autocompleteRef.current.getPlace();
      setSelectedPlace(place);
      console.log('Selected place:', place);
    }
  };
  
  return (
    <LoadScript googleMapsApiKey="YOUR_API_KEY" libraries={['places']}>
      <div>
        <div style={{ padding: '10px' }}>
          <Autocomplete onLoad={onLoad} onPlaceChanged={onPlaceChanged}>
            <input
              type="text"
              placeholder="Enter a place"
              style={{
                boxSizing: 'border-box',
                border: '1px solid transparent',
                width: '240px',
                height: '32px',
                padding: '0 12px',
                borderRadius: '3px',
                boxShadow: '0 2px 6px rgba(0, 0, 0, 0.3)',
                fontSize: '14px',
                outline: 'none'
              }}
            />
          </Autocomplete>
        </div>
        
        {selectedPlace && (
          <div style={{ padding: '10px', background: '#f0f0f0' }}>
            <h4>Selected Place:</h4>
            <div>Name: {selectedPlace.name}</div>
            <div>Address: {selectedPlace.formatted_address}</div>
            {selectedPlace.geometry && (
              <div>
                Coordinates: {selectedPlace.geometry.location?.lat()}, {selectedPlace.geometry.location?.lng()}
              </div>
            )}
          </div>
        )}
        
        <GoogleMap
          center={
            selectedPlace?.geometry?.location 
              ? selectedPlace.geometry.location.toJSON()
              : { lat: 40.7128, lng: -74.0060 }
          }
          zoom={15}
          mapContainerStyle={{ width: '100%', height: '400px' }}
        />
      </div>
    </LoadScript>
  );
}

// Advanced autocomplete with restrictions and fields
function AdvancedAutocomplete() {
  const [selectedPlace, setSelectedPlace] = useState<google.maps.places.PlaceResult | null>(null);
  const [country, setCountry] = useState<string>('us');
  const [placeType, setPlaceType] = useState<string>('establishment');
  const autocompleteRef = useRef<google.maps.places.Autocomplete | null>(null);
  
  // Specify which place data fields to retrieve
  const placeFields = [
    'place_id',
    'name',
    'formatted_address',
    'geometry',
    'types',
    'rating',
    'price_level',
    'opening_hours',
    'photos',
    'international_phone_number',
    'website'
  ];
  
  const placeTypes = [
    { value: 'establishment', label: 'Establishments' },
    { value: 'geocode', label: 'Geocodes' },
    { value: 'address', label: 'Addresses' },
    { value: '(cities)', label: 'Cities' },
    { value: '(regions)', label: 'Regions' }
  ];
  
  const onLoad = (autocomplete: google.maps.places.Autocomplete) => {
    autocompleteRef.current = autocomplete;
  };
  
  const onPlaceChanged = () => {
    if (autocompleteRef.current) {
      const place = autocompleteRef.current.getPlace();
      setSelectedPlace(place);
      console.log('Place details:', place);
    }
  };
  
  return (
    <LoadScript googleMapsApiKey="YOUR_API_KEY" libraries={['places']}>
      <div>
        <div style={{ padding: '10px', background: '#f0f0f0' }}>
          <div style={{ marginBottom: '10px' }}>
            <label style={{ marginRight: '10px' }}>
              Country: 
              <select 
                value={country} 
                onChange={(e) => setCountry(e.target.value)}
                style={{ marginLeft: '5px' }}
              >
                <option value="us">United States</option>
                <option value="ca">Canada</option>
                <option value="gb">United Kingdom</option>
                <option value="au">Australia</option>
                <option value="de">Germany</option>
                <option value="fr">France</option>
              </select>
            </label>
            
            <label>
              Place Type: 
              <select 
                value={placeType} 
                onChange={(e) => setPlaceType(e.target.value)}
                style={{ marginLeft: '5px' }}
              >
                {placeTypes.map(type => (
                  <option key={type.value} value={type.value}>
                    {type.label}
                  </option>
                ))}
              </select>
            </label>
          </div>
          
          <Autocomplete
            onLoad={onLoad}
            onPlaceChanged={onPlaceChanged}
            restrictions={{ country }}
            types={[placeType]}
            fields={placeFields}
            options={{
              strictBounds: false,
              componentRestrictions: { country }
            }}
          >
            <input
              type="text"
              placeholder={`Search for ${placeTypes.find(t => t.value === placeType)?.label.toLowerCase()} in ${country.toUpperCase()}`}
              style={{
                width: '100%',
                height: '40px',
                padding: '0 15px',
                fontSize: '16px',
                border: '2px solid #4285f4',
                borderRadius: '8px',
                outline: 'none',
                boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)'
              }}
            />
          </Autocomplete>
        </div>
        
        {selectedPlace && (
          <div style={{ padding: '15px', background: 'white', margin: '10px', borderRadius: '8px', boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)' }}>
            <h4>{selectedPlace.name}</h4>
            <div><strong>Address:</strong> {selectedPlace.formatted_address}</div>
            <div><strong>Types:</strong> {selectedPlace.types?.join(', ')}</div>
            
            {selectedPlace.rating && (
              <div><strong>Rating:</strong> {selectedPlace.rating} ⭐</div>
            )}
            
            {selectedPlace.price_level !== undefined && (
              <div><strong>Price Level:</strong> {'$'.repeat(selectedPlace.price_level + 1)}</div>
            )}
            
            {selectedPlace.opening_hours && (
              <div><strong>Open Now:</strong> {selectedPlace.opening_hours.open_now ? 'Yes' : 'No'}</div>
            )}
            
            {selectedPlace.international_phone_number && (
              <div><strong>Phone:</strong> {selectedPlace.international_phone_number}</div>
            )}
            
            {selectedPlace.website && (
              <div>
                <strong>Website:</strong> 
                <a href={selectedPlace.website} target="_blank" rel="noopener noreferrer" style={{ marginLeft: '5px' }}>
                  {selectedPlace.website}
                </a>
              </div>
            )}
            
            {selectedPlace.photos && selectedPlace.photos.length > 0 && (
              <div style={{ marginTop: '10px' }}>
                <strong>Photos:</strong>
                <div style={{ display: 'flex', gap: '10px', marginTop: '5px', overflow: 'auto' }}>
                  {selectedPlace.photos.slice(0, 3).map((photo, index) => (
                    <img
                      key={index}
                      src={photo.getUrl({ maxWidth: 200, maxHeight: 150 })}
                      alt={`${selectedPlace.name} photo ${index + 1}`}
                      style={{ borderRadius: '4px', flexShrink: 0 }}
                    />
                  ))}
                </div>
              </div>
            )}
          </div>
        )}
        
        <GoogleMap
          center={
            selectedPlace?.geometry?.location 
              ? selectedPlace.geometry.location.toJSON()
              : { lat: 40.7128, lng: -74.0060 }
          }
          zoom={selectedPlace ? 16 : 10}
          mapContainerStyle={{ width: '100%', height: '400px' }}
        >
          {selectedPlace && selectedPlace.geometry && (
            <Marker position={selectedPlace.geometry.location!.toJSON()} />
          )}
        </GoogleMap>
      </div>
    </LoadScript>
  );
}

// Autocomplete with bounds restriction
function BoundedAutocomplete() {
  const [mapCenter, setMapCenter] = useState({ lat: 40.7128, lng: -74.0060 });
  const [mapBounds, setMapBounds] = useState<google.maps.LatLngBounds | null>(null);
  const [selectedPlace, setSelectedPlace] = useState<google.maps.places.PlaceResult | null>(null);
  const autocompleteRef = useRef<google.maps.places.Autocomplete | null>(null);
  const mapRef = useRef<google.maps.Map | null>(null);
  
  const onMapLoad = (map: google.maps.Map) => {
    mapRef.current = map;
    updateBounds(map);
  };
  
  const updateBounds = (map: google.maps.Map) => {
    const bounds = map.getBounds();
    if (bounds) {
      setMapBounds(bounds);
    }
  };
  
  const onPlaceChanged = () => {
    if (autocompleteRef.current) {
      const place = autocompleteRef.current.getPlace();
      setSelectedPlace(place);
      
      if (place.geometry && place.geometry.location) {
        const location = place.geometry.location.toJSON();
        setMapCenter(location);
        
        // Fit map to place if it has a viewport
        if (place.geometry.viewport && mapRef.current) {
          mapRef.current.fitBounds(place.geometry.viewport);
        }
      }
    }
  };
  
  return (
    <LoadScript googleMapsApiKey="YOUR_API_KEY" libraries={['places']}>
      <div>
        <div style={{ padding: '10px', background: '#f0f0f0' }}>
          <div style={{ marginBottom: '10px' }}>
            <strong>Search within current map area:</strong>
          </div>
          
          <Autocomplete
            onLoad={(autocomplete) => {
              autocompleteRef.current = autocomplete;
            }}
            onPlaceChanged={onPlaceChanged}
            bounds={mapBounds || undefined}
            options={{
              strictBounds: true // Restrict results to the specified bounds
            }}
          >
            <input
              type="text"
              placeholder="Search places within map area"
              style={{
                width: '100%',
                height: '40px',
                padding: '0 15px',
                fontSize: '16px',
                border: '1px solid #ccc',
                borderRadius: '4px'
              }}
            />
          </Autocomplete>
          
          {selectedPlace && (
            <div style={{ marginTop: '10px', padding: '10px', background: 'white', borderRadius: '4px' }}>
              <strong>{selectedPlace.name}</strong>
              <div>{selectedPlace.formatted_address}</div>
            </div>
          )}
        </div>
        
        <GoogleMap
          center={mapCenter}
          zoom={13}
          mapContainerStyle={{ width: '100%', height: '400px' }}
          onLoad={onMapLoad}
          onBoundsChanged={() => {
            if (mapRef.current) {
              updateBounds(mapRef.current);
            }
          }}
        >
          {selectedPlace && selectedPlace.geometry && (
            <Marker position={selectedPlace.geometry.location!.toJSON()} />
          )}
        </GoogleMap>
      </div>
    </LoadScript>
  );
}

StandaloneSearchBox Component

Provides place search functionality without the autocomplete dropdown, useful for custom search interfaces.

/**
 * Provides place search functionality without autocomplete dropdown
 * Useful for custom search interfaces and batch place searching
 */
interface StandaloneSearchBoxProps {
  children: React.ReactNode; // Required - must contain exactly one input element
  bounds?: google.maps.LatLngBounds | google.maps.LatLngBoundsLiteral;
  options?: google.maps.places.SearchBoxOptions;
  
  // Event handlers
  onPlacesChanged?: () => void;
  
  // Lifecycle events
  onLoad?: (searchBox: google.maps.places.SearchBox) => void;
  onUnmount?: (searchBox: google.maps.places.SearchBox) => void;
}

interface google.maps.places.SearchBoxOptions {
  bounds?: google.maps.LatLngBounds | google.maps.LatLngBoundsLiteral;
}

function StandaloneSearchBox(props: StandaloneSearchBoxProps): JSX.Element;

Usage Examples:

import React, { useState, useRef } from 'react';
import { GoogleMap, LoadScript, StandaloneSearchBox, Marker } from '@react-google-maps/api';

// Basic search box
function BasicSearchBox() {
  const [places, setPlaces] = useState<google.maps.places.PlaceResult[]>([]);
  const [mapCenter, setMapCenter] = useState({ lat: 40.7128, lng: -74.0060 });
  const searchBoxRef = useRef<google.maps.places.SearchBox | null>(null);
  
  const onLoad = (searchBox: google.maps.places.SearchBox) => {
    searchBoxRef.current = searchBox;
  };
  
  const onPlacesChanged = () => {
    if (searchBoxRef.current) {
      const places = searchBoxRef.current.getPlaces();
      
      if (places && places.length > 0) {
        setPlaces(places);
        
        // Center map on first result
        const firstPlace = places[0];
        if (firstPlace.geometry && firstPlace.geometry.location) {
          setMapCenter(firstPlace.geometry.location.toJSON());
        }
        
        console.log('Search results:', places);
      }
    }
  };
  
  return (
    <LoadScript googleMapsApiKey="YOUR_API_KEY" libraries={['places']}>
      <div>
        <div style={{ padding: '10px' }}>
          <StandaloneSearchBox onLoad={onLoad} onPlacesChanged={onPlacesChanged}>
            <input
              type="text"
              placeholder="Search for places"
              style={{
                width: '100%',
                height: '40px',
                padding: '0 15px',
                fontSize: '16px',
                border: '2px solid #4285f4',
                borderRadius: '8px',
                outline: 'none'
              }}
            />
          </StandaloneSearchBox>
        </div>
        
        {places.length > 0 && (
          <div style={{ padding: '10px', background: '#f0f0f0' }}>
            <h4>Search Results ({places.length}):</h4>
            <div style={{ maxHeight: '200px', overflowY: 'auto' }}>
              {places.map((place, index) => (
                <div key={index} style={{ padding: '5px', borderBottom: '1px solid #ccc' }}>
                  <strong>{place.name}</strong>
                  <div>{place.formatted_address}</div>
                </div>
              ))}
            </div>
          </div>
        )}
        
        <GoogleMap
          center={mapCenter}
          zoom={15}
          mapContainerStyle={{ width: '100%', height: '400px' }}
        >
          {places.map((place, index) => 
            place.geometry && place.geometry.location && (
              <Marker
                key={index}
                position={place.geometry.location.toJSON()}
                title={place.name}
              />
            )
          )}
        </GoogleMap>
      </div>
    </LoadScript>
  );
}

// Advanced search with filtering and categorization
function AdvancedSearchBox() {
  const [places, setPlaces] = useState<google.maps.places.PlaceResult[]>([]);
  const [filteredPlaces, setFilteredPlaces] = useState<google.maps.places.PlaceResult[]>([]);
  const [selectedCategory, setSelectedCategory] = useState<string>('all');
  const [mapBounds, setMapBounds] = useState<google.maps.LatLngBounds | null>(null);
  const searchBoxRef = useRef<google.maps.places.SearchBox | null>(null);
  const mapRef = useRef<google.maps.Map | null>(null);
  
  const categories = [
    { value: 'all', label: 'All Places' },
    { value: 'restaurant', label: 'Restaurants' },
    { value: 'lodging', label: 'Hotels' },
    { value: 'tourist_attraction', label: 'Attractions' },
    { value: 'store', label: 'Stores' },
    { value: 'bank', label: 'Banks' },
    { value: 'hospital', label: 'Hospitals' }
  ];
  
  const onPlacesChanged = () => {
    if (searchBoxRef.current) {
      const searchResults = searchBoxRef.current.getPlaces();
      
      if (searchResults && searchResults.length > 0) {
        setPlaces(searchResults);
        filterPlaces(searchResults, selectedCategory);
        
        // Fit map to show all results
        if (mapRef.current && searchResults.length > 1) {
          const bounds = new google.maps.LatLngBounds();
          searchResults.forEach(place => {
            if (place.geometry && place.geometry.location) {
              bounds.extend(place.geometry.location);
            }
          });
          mapRef.current.fitBounds(bounds);
        }
      }
    }
  };
  
  const filterPlaces = (placesToFilter: google.maps.places.PlaceResult[], category: string) => {
    if (category === 'all') {
      setFilteredPlaces(placesToFilter);
    } else {
      const filtered = placesToFilter.filter(place => 
        place.types?.includes(category as any)
      );
      setFilteredPlaces(filtered);
    }
  };
  
  React.useEffect(() => {
    filterPlaces(places, selectedCategory);
  }, [selectedCategory, places]);
  
  const getMarkerColor = (place: google.maps.places.PlaceResult) => {
    const types = place.types || [];
    if (types.includes('restaurant' as any)) return 'red';
    if (types.includes('lodging' as any)) return 'blue';
    if (types.includes('tourist_attraction' as any)) return 'green';
    if (types.includes('store' as any)) return 'orange';
    return 'gray';
  };
  
  return (
    <LoadScript googleMapsApiKey="YOUR_API_KEY" libraries={['places']}>
      <div>
        <div style={{ padding: '10px', background: '#f0f0f0' }}>
          <div style={{ marginBottom: '10px' }}>
            <StandaloneSearchBox 
              onLoad={(searchBox) => { searchBoxRef.current = searchBox; }}
              onPlacesChanged={onPlacesChanged}
              bounds={mapBounds || undefined}
            >
              <input
                type="text"
                placeholder="Search for restaurants, hotels, attractions..."
                style={{
                  width: '100%',
                  height: '40px',
                  padding: '0 15px',
                  fontSize: '16px',
                  border: '1px solid #ccc',
                  borderRadius: '4px'
                }}
              />
            </StandaloneSearchBox>
          </div>
          
          <div style={{ marginBottom: '10px' }}>
            <label>Filter by category: </label>
            <select 
              value={selectedCategory} 
              onChange={(e) => setSelectedCategory(e.target.value)}
              style={{ marginLeft: '10px', padding: '5px' }}
            >
              {categories.map(category => (
                <option key={category.value} value={category.value}>
                  {category.label}
                </option>
              ))}
            </select>
          </div>
          
          {filteredPlaces.length > 0 && (
            <div>
              <div>Showing {filteredPlaces.length} of {places.length} places</div>
            </div>
          )}
        </div>
        
        {filteredPlaces.length > 0 && (
          <div style={{ 
            height: '150px', 
            overflowY: 'auto', 
            padding: '10px', 
            background: 'white',
            borderTop: '1px solid #ccc'
          }}>
            {filteredPlaces.map((place, index) => (
              <div 
                key={index} 
                style={{ 
                  padding: '8px', 
                  borderBottom: '1px solid #eee',
                  cursor: 'pointer',
                  display: 'flex',
                  alignItems: 'center'
                }}
                onClick={() => {
                  if (place.geometry && place.geometry.location && mapRef.current) {
                    mapRef.current.setCenter(place.geometry.location);
                    mapRef.current.setZoom(16);
                  }
                }}
              >
                <div 
                  style={{
                    width: '12px',
                    height: '12px',
                    borderRadius: '50%',
                    backgroundColor: getMarkerColor(place),
                    marginRight: '10px',
                    flexShrink: 0
                  }}
                />
                <div>
                  <div><strong>{place.name}</strong></div>
                  <div style={{ fontSize: '12px', color: '#666' }}>
                    {place.formatted_address}
                  </div>
                  {place.rating && (
                    <div style={{ fontSize: '12px', color: '#666' }}>
                      Rating: {place.rating} ⭐
                    </div>
                  )}
                </div>
              </div>
            ))}
          </div>
        )}
        
        <GoogleMap
          center={{ lat: 40.7128, lng: -74.0060 }}
          zoom={13}
          mapContainerStyle={{ width: '100%', height: '400px' }}
          onLoad={(map) => {
            mapRef.current = map;
          }}
          onBoundsChanged={() => {
            if (mapRef.current) {
              const bounds = mapRef.current.getBounds();
              if (bounds) {
                setMapBounds(bounds);
              }
            }
          }}
        >
          {filteredPlaces.map((place, index) => 
            place.geometry && place.geometry.location && (
              <Marker
                key={index}
                position={place.geometry.location.toJSON()}
                title={place.name}
                icon={`https://maps.google.com/mapfiles/ms/icons/${getMarkerColor(place)}-dot.png`}
              />
            )
          )}
        </GoogleMap>
      </div>
    </LoadScript>
  );
}

// Search box with custom UI and place details
function CustomSearchInterface() {
  const [places, setPlaces] = useState<google.maps.places.PlaceResult[]>([]);
  const [selectedPlace, setSelectedPlace] = useState<google.maps.places.PlaceResult | null>(null);
  const [searchValue, setSearchValue] = useState('');
  const searchBoxRef = useRef<google.maps.places.SearchBox | null>(null);
  
  const performSearch = () => {
    if (searchBoxRef.current && searchValue.trim()) {
      // Trigger search by setting the input value and firing the event
      const input = document.getElementById('search-input') as HTMLInputElement;
      if (input) {
        input.value = searchValue;
        google.maps.event.trigger(input, 'focus');
        google.maps.event.trigger(input, 'keydown', { keyCode: 13 });
      }
    }
  };
  
  const onPlacesChanged = () => {
    if (searchBoxRef.current) {
      const results = searchBoxRef.current.getPlaces();
      if (results && results.length > 0) {
        setPlaces(results);
        setSelectedPlace(results[0]); // Select first result by default
      }
    }
  };
  
  return (
    <LoadScript googleMapsApiKey="YOUR_API_KEY" libraries={['places']}>
      <div style={{ display: 'flex', height: '500px' }}>
        {/* Search Panel */}
        <div style={{ width: '350px', padding: '15px', background: '#f8f9fa', borderRight: '1px solid #dee2e6' }}>
          <h3 style={{ margin: '0 0 15px 0' }}>Place Search</h3>
          
          <div style={{ marginBottom: '15px' }}>
            <StandaloneSearchBox 
              onLoad={(searchBox) => { searchBoxRef.current = searchBox; }}
              onPlacesChanged={onPlacesChanged}
            >
              <input
                id="search-input"
                type="text"
                value={searchValue}
                onChange={(e) => setSearchValue(e.target.value)}
                onKeyPress={(e) => e.key === 'Enter' && performSearch()}
                placeholder="Search places..."
                style={{
                  width: '100%',
                  height: '40px',
                  padding: '0 15px',
                  border: '1px solid #ced4da',
                  borderRadius: '4px',
                  fontSize: '14px'
                }}
              />
            </StandaloneSearchBox>
            
            <button
              onClick={performSearch}
              style={{
                width: '100%',
                marginTop: '10px',
                padding: '10px',
                background: '#007bff',
                color: 'white',
                border: 'none',
                borderRadius: '4px',
                cursor: 'pointer'
              }}
            >
              Search
            </button>
          </div>
          
          {/* Results List */}
          <div style={{ maxHeight: '300px', overflowY: 'auto' }}>
            {places.map((place, index) => (
              <div
                key={index}
                onClick={() => setSelectedPlace(place)}
                style={{
                  padding: '10px',
                  margin: '5px 0',
                  background: selectedPlace === place ? '#e3f2fd' : 'white',
                  border: '1px solid #dee2e6',
                  borderRadius: '4px',
                  cursor: 'pointer'
                }}
              >
                <div style={{ fontWeight: 'bold', marginBottom: '5px' }}>
                  {place.name}
                </div>
                <div style={{ fontSize: '12px', color: '#666' }}>
                  {place.formatted_address}
                </div>
                {place.rating && (
                  <div style={{ fontSize: '12px', marginTop: '5px' }}>
                    ⭐ {place.rating} {place.user_ratings_total && `(${place.user_ratings_total} reviews)`}
                  </div>
                )}
              </div>
            ))}
          </div>
          
          {/* Selected Place Details */}
          {selectedPlace && (
            <div style={{ 
              marginTop: '15px', 
              padding: '15px', 
              background: 'white', 
              border: '1px solid #dee2e6', 
              borderRadius: '4px' 
            }}>
              <h4 style={{ margin: '0 0 10px 0' }}>Place Details</h4>
              <div><strong>Name:</strong> {selectedPlace.name}</div>
              <div><strong>Address:</strong> {selectedPlace.formatted_address}</div>
              <div><strong>Types:</strong> {selectedPlace.types?.join(', ')}</div>
              {selectedPlace.rating && (
                <div><strong>Rating:</strong> {selectedPlace.rating} / 5</div>
              )}
              {selectedPlace.price_level !== undefined && (
                <div><strong>Price:</strong> {'$'.repeat(selectedPlace.price_level + 1)}</div>
              )}
            </div>
          )}
        </div>
        
        {/* Map */}
        <div style={{ flex: 1 }}>
          <GoogleMap
            center={
              selectedPlace?.geometry?.location
                ? selectedPlace.geometry.location.toJSON()
                : { lat: 40.7128, lng: -74.0060 }
            }
            zoom={selectedPlace ? 16 : 12}
            mapContainerStyle={{ width: '100%', height: '100%' }}
          >
            {places.map((place, index) => 
              place.geometry && place.geometry.location && (
                <Marker
                  key={index}
                  position={place.geometry.location.toJSON()}
                  title={place.name}
                  icon={{
                    url: selectedPlace === place
                      ? 'https://maps.google.com/mapfiles/ms/icons/red-dot.png'
                      : 'https://maps.google.com/mapfiles/ms/icons/blue-dot.png',
                    scaledSize: new google.maps.Size(32, 32)
                  }}
                  onClick={() => setSelectedPlace(place)}
                />
              )
            )}
          </GoogleMap>
        </div>
      </div>
    </LoadScript>
  );
}

Places API Integration Best Practices

Guidelines for effective Places API usage and optimization strategies.

/**
 * Places API integration best practices and optimization
 */
interface PlacesAPIBestPractices {
  // Field selection optimization
  requestOnlyNeededFields: boolean;    // Reduce API costs by requesting specific fields
  essentialFields: string[];           // Minimal required fields for basic functionality
  extendedFields: string[];           // Additional fields for enhanced features
  
  // Geographic optimization
  useBounds: boolean;                 // Restrict searches to relevant geographic areas
  useStrictBounds: boolean;           // Enforce strict boundary compliance
  
  // Performance optimization
  implementDebouncing: boolean;       // Debounce user input to reduce API calls
  cacheResults: boolean;              // Cache place results for repeated queries
  useSessionTokens: boolean;          // Group related requests for cost optimization
}

// Example field optimization
const PLACE_FIELDS = {
  basic: ['place_id', 'name', 'formatted_address', 'geometry'],
  contact: ['international_phone_number', 'website', 'opening_hours'],
  atmosphere: ['rating', 'user_ratings_total', 'price_level', 'reviews'],
  media: ['photos', 'icon', 'icon_background_color'],
  detailed: ['types', 'vicinity', 'plus_code', 'url']
};

// Example debounced autocomplete implementation
const useDebouncedAutocomplete = (delay: number = 300) => {
  const [searchTerm, setSearchTerm] = React.useState('');
  const [debouncedTerm, setDebouncedTerm] = React.useState('');
  
  React.useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedTerm(searchTerm);
    }, delay);
    
    return () => clearTimeout(timer);
  }, [searchTerm, delay]);
  
  return { searchTerm, setSearchTerm, debouncedTerm };
};

Install with Tessl CLI

npx tessl i tessl/npm-react-google-maps--api

docs

clustering.md

core-map.md

drawing-shapes.md

index.md

layers.md

markers-overlays.md

places.md

script-loading.md

services.md

tile.json