Google Maps in React with Places Search Bar, Draggable Multiple Marker using google-map-react

In this React tutorial, we’re going to embed a Google Map with advanced feature components like Draggable Marker, Fetch Address of current point using Geocoder and Place search, or Autocomplete in React application by using a widely used and popular third-party package name google-map-react.

Google Maps javascript API provides a number of methods and features to create awesome and super interactive Geolocation based maps. In the React application, where we have a composition of sections for each control, we cannot simply use this API.

To implement Google Maps in a React application we are going to use a third party packages module named google-map-react.

The google-map-react package can be used to create custom components for embedding an interactive and fully-features Google map on a page.

 

Today we are going to create Google maps having the following features:

  • Get the current position using navigator API to set the center of Map.
  • Add Autocomplete/ Search bar for places to locate the map center for selected places.
  • Draggable Marker in Map, to show coordinates of the current position.
  • Fetch address using Geocoder service on load and change of marker position.
  • For touch devices, a user can tab on the Map instead of drag to change position.

 

 

Create a React Application

First, we’ll create a new React application using npx create-react-app command

$ npx create-react-app react-google-maps-app

Move inside the react app

$ cd react-google-maps-app

Run application

$ npm start

 

Install Required Package

For using Google Maps API component in React application, install the google-map-react package.

$ npm install --save google-map-react

Also, we’ll install the styled-components package, this is used to add in-component styling to the components.

$ npm install --save styled-components

 

Create New Components

Before we start, create three new class components for Marker, Autocomplete, and GoogleMap. In the components folder create the following files:

 

  • ‘~components/Marker.js’
  • ‘~components/Autocomplete.js’
  • ‘~components/MyGoogleMap.js’

We’ll use Marker and Autocomplete components inside the MyGoogleMap components.

 

Update Marker Functional Component

The Marker component will handle the click events, having local style using styled-components.

Update the ‘~components/Marker.js‘ file with the following code:

// Marker.js
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';

const Wrapper = styled.div`
    position: absolute;
    width: 38px;
    height: 37px;
    background-image: url(https://icon-library.com/images/pin-icon-png/pin-icon-png-9.jpg);
    background-size: contain;
    background-repeat: no-repeat;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    -webkit-transform: translate(-50%,-50%);
    -ms-transform: translate(-50%,-50%);
    transform: translate(-50%,-50%);
    cursor: grab;
`;

const Marker = ({ text, onClick }) => (
    <Wrapper
        alt={text}
        onClick={onClick}
    />
);

Marker.defaultProps = {
    onClick: null,
};

Marker.propTypes = {
    onClick: PropTypes.func,
    text: PropTypes.string.isRequired,
};

export default Marker;

 

Update Autocomplete Class Component

Now, we’ll update the Autocomplete class component. This will have an event listener for place changed using the places API.

This will render an <input /> control event to clear query and also return the selected place from Autosuggestion to parent component.

Update the ‘~components/Autocomplete.js’ file with the following code:

// Autocomplete.js
import React, { Component } from 'react';
import styled from 'styled-components';

const Wrapper = styled.div`
  position: relative;
  align-items: center;
  justify-content: center;
  width: 100%;
  padding: 20px;
  text-align:center;
`;

class AutoComplete extends Component {
    constructor(props) {
        super(props);
        this.clearSearchBox = this.clearSearchBox.bind(this);
    }

    componentDidMount({ map, mapApi } = this.props) {
        const options = {
            // restrict your search to a specific type of result
            types: ['address'],
            // restrict your search to a specific country, or an array of countries
            // componentRestrictions: { country: ['gb', 'us'] },
        };
        this.autoComplete = new mapApi.places.Autocomplete(
            this.searchInput,
            options,
        );
        this.autoComplete.addListener('place_changed', this.onPlaceChanged);
        this.autoComplete.bindTo('bounds', map);
    }

    componentWillUnmount({ mapApi } = this.props) {
        mapApi.event.clearInstanceListeners(this.searchInput);
    }

    onPlaceChanged = ({ map, addplace } = this.props) => {
        const place = this.autoComplete.getPlace();

        if (!place.geometry) return;
        if (place.geometry.viewport) {
            map.fitBounds(place.geometry.viewport);
        } else {
            map.setCenter(place.geometry.location);
            map.setZoom(17);
        }

        addplace(place);
        this.searchInput.blur();
    };

    clearSearchBox() {
        this.searchInput.value = '';
    }

    render() {
        return (
            <Wrapper>
                <input
                    className="search-input"
                    ref={(ref) => {
                        this.searchInput = ref;
                    }}
                    type="text"
                    onFocus={this.clearSearchBox}
                    placeholder="Enter a location"
                />
            </Wrapper>
        );
    }
}

export default AutoComplete;

 

Update MyGoogleMap Component

Finally, we’ll update the MyGoogleMap component to embed the Google Map.

# Initialize Class State

In this component class, we’ll have the state to control Google map APIs and coordinates.

state = {
        mapApiLoaded: false,
        mapInstance: null,
        mapApi: null,
        geoCoder: null,
        places: [],
        center: [],
        zoom: 9,
        address: '',
        draggable: true,
        lat: null,
        lng: null
    };

 

# Get Browser Geolocation

By using the browser’s navigator object we’ll fetch the current position of the user.

setCurrentLocation() {
        if ('geolocation' in navigator) {
            navigator.geolocation.getCurrentPosition((position) => {
                this.setState({
                    center: [position.coords.latitude, position.coords.longitude],
                    lat: position.coords.latitude,
                    lng: position.coords.longitude
                });
            });
        }
    }

And call this method inside the componentWillMount() hook.

componentWillMount() {
        this.setCurrentLocation();
    }

 

# Render Google Map

Inside the render method of the class, we’ll now add the <Autocomplete /> component and pass google API instances to it using props.

<AutoComplete map={mapInstance} mapApi={mapApi} addplace={this.addPlace} />

 

The Google map will be rendered by adding the <GoogleMapReact /> component. This will be enriched with many properties and event handlers.

<GoogleMapReact
                    center={this.state.center}
                    zoom={this.state.zoom}
                    draggable={this.state.draggable}
                    onChange={this._onChange}
                    onChildMouseDown={this.onMarkerInteraction}
                    onChildMouseUp={this.onMarkerInteractionMouseUp}
                    onChildMouseMove={this.onMarkerInteraction}
                    onChildClick={() => console.log('child click')}
                    onClick={this._onClick}
                    bootstrapURLKeys={{
                        key: 'YOUR_API_KEY',
                        libraries: ['places', 'geometry'],
                    }}
                    yesIWantToUseGoogleMapApiInternals
                    onGoogleApiLoaded={({ map, maps }) => this.apiHasLoaded(map, maps)}
                >

                    <Marker
                        text={this.state.address}
                        lat={this.state.lat}
                        lng={this.state.lng}
                    />


 </GoogleMapReact>

let’s have a look at properties used:

bootstrapURLKeys: This object will have the Google API key and the libraries you need.

yesIWantToUseGoogleMapApiInternals: For adding a custom Map initialize method we need to add this property.

onGoogleApiLoaded: This event handler is triggered when Google Map API is loaded.

There are also some event handlers on Map and its internal components like Marker.

 

# Add Marker Component

Inside the <GoogleMapReact/> component, add the <Marker/> component with location coordinates. These coordinates will be updated on load, drag, and place search change when the internal state is updated.

 

# Show Address using Geocode Service API

We are also singing the Geocode service to fetch address by passing the coordinates. The addresses array is getting converted into a readable string with the help of the _generateAddress() function.

_generateAddress() {
        const {
            mapApi
        } = this.state;

        const geocoder = new mapApi.Geocoder;

        geocoder.geocode({ 'location': { lat: this.state.lat, lng: this.state.lng } }, (results, status) => {
            console.log(results);
            console.log(status);
            if (status === 'OK') {
                if (results[0]) {
                    this.zoom = 12;
                    this.setState({ address: results[0].formatted_address });
                } else {
                    window.alert('No results found');
                }
            } else {
                window.alert('Geocoder failed due to: ' + status);
            }

        });
    }

 

# Finally Update the MyGoogleMap Component

The MyGoogleMap component will have the following code:

// MyGoogleMaps.js
import React, { Component } from 'react';

import GoogleMapReact from 'google-map-react';

import styled from 'styled-components';

import AutoComplete from './Autocomplete';
import Marker from './Marker';

const Wrapper = styled.main`
  width: 100%;
  height: 100%;
`;

class MyGoogleMap extends Component {


    state = {
        mapApiLoaded: false,
        mapInstance: null,
        mapApi: null,
        geoCoder: null,
        places: [],
        center: [],
        zoom: 9,
        address: '',
        draggable: true,
        lat: null,
        lng: null
    };

    componentWillMount() {
        this.setCurrentLocation();
    }


    onMarkerInteraction = (childKey, childProps, mouse) => {
        this.setState({
            draggable: false,
            lat: mouse.lat,
            lng: mouse.lng
        });
    }
    onMarkerInteractionMouseUp = (childKey, childProps, mouse) => {
        this.setState({ draggable: true });
        this._generateAddress();
    }

    _onChange = ({ center, zoom }) => {
        this.setState({
            center: center,
            zoom: zoom,
        });

    }

    _onClick = (value) => {
        this.setState({
            lat: value.lat,
            lng: value.lng
        });
    }

    apiHasLoaded = (map, maps) => {
        this.setState({
            mapApiLoaded: true,
            mapInstance: map,
            mapApi: maps,
        });

        this._generateAddress();
    };

    addPlace = (place) => {
        this.setState({
            places: [place],
            lat: place.geometry.location.lat(),
            lng: place.geometry.location.lng()
        });
        this._generateAddress()
    };

    _generateAddress() {
        const {
            mapApi
        } = this.state;

        const geocoder = new mapApi.Geocoder;

        geocoder.geocode({ 'location': { lat: this.state.lat, lng: this.state.lng } }, (results, status) => {
            console.log(results);
            console.log(status);
            if (status === 'OK') {
                if (results[0]) {
                    this.zoom = 12;
                    this.setState({ address: results[0].formatted_address });
                } else {
                    window.alert('No results found');
                }
            } else {
                window.alert('Geocoder failed due to: ' + status);
            }

        });
    }

    // Get Current Location Coordinates
    setCurrentLocation() {
        if ('geolocation' in navigator) {
            navigator.geolocation.getCurrentPosition((position) => {
                this.setState({
                    center: [position.coords.latitude, position.coords.longitude],
                    lat: position.coords.latitude,
                    lng: position.coords.longitude
                });
            });
        }
    }

    render() {
        const {
            places, mapApiLoaded, mapInstance, mapApi,
        } = this.state;


        return (
            <Wrapper>
                {mapApiLoaded && (
                    <div>
                        <AutoComplete map={mapInstance} mapApi={mapApi} addplace={this.addPlace} />
                    </div>
                )}
                <GoogleMapReact
                    center={this.state.center}
                    zoom={this.state.zoom}
                    draggable={this.state.draggable}
                    onChange={this._onChange}
                    onChildMouseDown={this.onMarkerInteraction}
                    onChildMouseUp={this.onMarkerInteractionMouseUp}
                    onChildMouseMove={this.onMarkerInteraction}
                    onChildClick={() => console.log('child click')}
                    onClick={this._onClick}
                    bootstrapURLKeys={{
                        key: 'AIzaSyAM9uE4Sy2nWFfP-Ha6H8ZC6ghAMKJEKps',
                        libraries: ['places', 'geometry'],
                    }}
                    yesIWantToUseGoogleMapApiInternals
                    onGoogleApiLoaded={({ map, maps }) => this.apiHasLoaded(map, maps)}
                >

                    <Marker
                        text={this.state.address}
                        lat={this.state.lat}
                        lng={this.state.lng}
                    />


                </GoogleMapReact>

                <div className="info-wrapper">
                    <div className="map-details">Latitude: <span>{this.state.lat}</span>, Longitude: <span>{this.state.lng}</span></div>
                    <div className="map-details">Zoom: <span>{this.state.zoom}</span></div>
                    <div className="map-details">Address: <span>{this.state.address}</span></div>
                </div>


            </Wrapper >
        );
    }
}

export default MyGoogleMap;

 

# Add CSS Style

In the App.css file add following CSS style for map components.

.main-wrapper {
  height: 60vh;
  margin: 10px 50px;
  filter: drop-shadow(-1px 5px 3px #ccc);
}
.info-wrapper {
  margin-top: 15px;
}
.map-details {
  text-align: center;
  font-size: 1.2em;
}
.map-details span {
  font-weight: bold;
}
.search-input {
  font-size: 1.2em;
  width: 80%;
}

 

Adding Google Map in App

After creating the components, we need to show it in the React application’s App page. To render the Google map inside the application, update the App.js file with the following code:

// App.js
import React from 'react';
import './App.css';
import MyGoogleMap from './components/MyGoogleMap';


function App() {

  return (
    <div className="main-wrapper">
      <MyGoogleMap />
    </div>
  );
}

export default App;

 

That’s it, now run the application by hitting $ npm start to open the app in the browser.

 

Conclusion

Finally, we’are done with the implementation of Google Maps in React application. Here we discussed how to add draggable Marker in Google map with Autocomplete place search. The draggable marker doesn’t work on touch devices so we used the map click event to update the Marker’s position when a user taps anywhere on the map.

Below the map, we are showing current marker position coordinates, Zoom level, and address which is converted to readable form using a helper function.

Hope this was helpful, do share your comments and feedback.

 

Leave a Comment

Your email address will not be published. Required fields are marked *