Creating a Google Map component in Rails-React
Tags: googlemaps, react, rails, beginners
Published: 2021-11-12 14:38:20

## Table of Contents

<hr/>

1. [React Google Maps API](#1-react-google-maps-api)
2. [Creating a Map component](#2-creating-a-map-component)
3. [Building our controller logic](#3-building-our-controller-logic)
4. [Making markers for our places](#4-making-markers-for-our-places)
5. [Adding infowindows to markers](#5-adding-infowindows-to-markers)
6. [Next steps](#6-next-steps)
7. [Get in touch](#7-get-in-touch)

<hr/>

## 1. React Google Maps API

<hr/>

Let's jump straight into building out our map. To start with we'll need to install [React Google Maps API](https://react-google-maps-api-docs.netlify.app/), a React package that gives us a handy wrapper around the Google Maps API that gives us a range of pre-built components that we can customise to our needs.

I'll be using npm for my installation but feel free to use yarn if that's what you're more comfortable with. We'll be following the instructions from the [package documentation](https://react-google-maps-api-docs.netlify.app/#section-getting-started) so head to your terminal and enter:

```
npm install --save @react-google-maps/api

# or

yarn add @react-google-maps/api
```

<hr/>

## 2. Creating a Map component

<hr/>

Once the install is complete we're going to create our Map component. In your terminal type:

```
rails g react:component Map
```

This will work exactly the same as the HelloWorld component we created previously, creating a new file for us in the `rails-react-google-maps/app/javascript/components/` folder. We'll be using the functional component provided in the docs so in your new `Map.js` component file, delete all the contents and copy in the following setup:

```
import React from 'react'
import { GoogleMap } from '@react-google-maps/api';

const containerStyle = {
  width: '100vw',
  height: '50vh'
};

const center = {
  lat: -3.745,
  lng: -38.523
};

const Map = () => {
  return (
    <GoogleMap
      mapContainerStyle={containerStyle}
      center={center}
      zoom={10}
    >
      { /* Child components, such as markers, info windows, etc. */ }
      <></>
    </GoogleMap>
  )
}

export default React.memo(Map)
```

You can technically give the component any name you want but for simplicity's sake we'll stick with Map.

Next let's get our map onto our page! Go back to your `index.html.erb` file and replace the existing HelloWorld components with the following line (P.S. you can also delete the `HelloWorld.js` file in your components folder at this point):

```
<%= react_component("Map") %>
```

Restart your rails server (`Ctrl+C` -> `rails s`) and refresh your `localhost:3000` page in your browser. Wait, where's our map?! You should probably see something like this:

![Map error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hvhao286z7j2v50aurl6.png)

Don't worry, this is a good sign since it means our map component is working! Let's check our browser's developer tools console to see what's happening:

![Console errors](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2mnl8kxw4l6v4do85q2a.png)

We're still missing something: our API keys. We need the keys we generated earlier and put in our `.env` file in order to gain access to Google Maps.

Back in our `application.html.erb` view file, paste this line of code inside the body tag. Everything should look like this:

```
<body>
  <%= yield %>
  <%= javascript_include_tag "https://maps.googleapis.com/maps/api/js?libraries=places&key=#{ENV['GMAPS_BROWSER_KEY']}" %>
</body>
```

This script will load Google Maps using our browser API key. We're not quite there yet, we still have one more unused key to place somewhere! This time we'll be revisiting our geocoder gem. Go back to `rails-react-google-maps/config/initializers/geocoder.rb` and uncomment the following lines:

```
lookup: :nominatim,         # name of geocoding service (symbol)
use_https: false,           # use HTTPS for lookup requests? (if supported)
api_key: nil,               # API key for geocoding service
```

Next, change the value of each of these to:

```
lookup: :google,
use_https: true,
api_key: ENV['GMAPS_SERVER_KEY'],
```

Refresh your `localhost:3000` page and you should have a map showing up. It's centered on Brazil for now due to the default coordinates in `Map.js` but we'll fix that next in the `places_controller.rb` file.

![Google Map](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xenwmaql3ytfywh65btq.png)

<hr/>

## 3. Building our controller logic

<hr/>

Our controller is like the brains of the operation, it's connecting our view (and the React components within) with the model and each instance of place. In Rails, the index action is where we access all of the instances of a class. In this case our class is Place and our instances are the 5 locations we seeded earlier (e.g. The White House).

<hr/>

### 3.1. Showing our places

<hr/>

The first thing we need to do is make sure our index page can read our places. Go back to `places_controller.rb` and add this line of code in the `index` action:

```
def index
  @places = Place.where.not(latitude: nil, longitude: nil)
end
```

In our view when we want to access our places data we can call `@places` and it should return every instance. Using `.where.not(latitude: nil, longitude: nil)` reads almost as we would say it in layman's terms - we only want places where the coordinates are not nil, or empty. If they're empty they'll be excluded from the results since we need coordinates for our map markers.

For some visual feedback, let's quickly add a few simple `erb` and HTML elements to our `index.html.erb` page. Underneath the map component, add the following:

```
<% @places.each do |place| %>
  <h2><%= place.name %></h2>
  <p><%= place.address %></p>
<% end %>
```

Here we use an each loop to iterate over our places and create an H2 and paragraph with the name and address respectively. It should look something like this:

![Map and places](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pr2t0lmql54no2s6yslu.png)

<hr/>

### 3.2. Sorting our controller logic

<hr/>

We can access whatever information we pass in the index action of our controller. At the moment our default center is sitting over Fortaleza, Brazil but we want to make this dynamic. Thankfully props in React play well with Rails to allow us to pass conditional data which means, depending on how many places we have or where they are, we can change where our map drops us.

In our `index.html.erb` file where we call our Map component let's add a few properties:

```
<%= react_component("Map", {
  center: @map_center,
  zoom: @map_zoom,
  markers: [@markers]
}) %>
```
Of course these properties don't actually exist yet so let's add them to our controller. Let's start with markers. In the React component above we pass them as an array - this is so that we can iterate over them to create individual markers.

#### Markers

In our controller's index action we'll do some more geocoding:

```
    @markers = @places.geocoded.map do |place|
      {
        id: place.id,
        lat: place.latitude,
        lng: place.longitude,
        name: place.name,
        address: place.address
      }
    end
```

The important information we need from here are the coordinates (lat, lng) and the id (for unique keys in React). I've added the name and address for future Google Maps InfoWindow components but we can ignore these for now.

#### Center and zoom

@map_center and @map_zoom are a little bit more complicated as we'll want these to change based on certain criteria. For example if there are no markers to display, we need a default place to land on. If we have only one place we want our map to center on that spot, and if we have more than one maybe we want a general area encompassing all our places. That's a lot of ifs that we can turn into a conditional:

```
if @places.count.zero?
  @map_center = [38.9072, 77.0369] # Washington D.C.
  @map_zoom = 0
elsif @places.count == 1
  @map_center = [@places[0].latitude, @places[0].longitude]
  @map_zoom = 14
else
  avg_lat = 0
  avg_lon = 0

  @places.map do |place|
    avg_lat += place.latitude
    avg_lon += place.longitude
  end

  @map_center = [(avg_lat / @places.count), (avg_lon / @places.count)]
  @map_zoom = 12
end
```

That's a lot to take in so let's break it down into bite-sized pieces:

```
if @places.count.zero?
  @map_center = [38.9072, 77.0369] # Washington D.C.
  @map_zoom = 0
```

Here we're saying that if there are no places to add to the map, set our default center to the middle of Washington D.C. I did this because our markers are all based there but you can change these defaults to wherever you like. For zoom, the higher the number, the closer the zoom.

```
elsif @places.count == 1
  @map_center = [@places[0].latitude, @places[0].longitude]
  @map_zoom = 14
```

The same idea as above except this time we're checking if there's only one place. If so, we want our map to be centered on that place's coordinates (`[@places[0].latitude, @places[0].longitude]`) and zoomed in on a closer area.

```
else
  avg_lat = 0
  avg_lon = 0

  @places.map do |place|
    avg_lat += place.latitude
    avg_lon += place.longitude
  end

  @map_center = [(avg_lat / @places.count), (avg_lon / @places.count)]
  @map_zoom = 12
end
```

If we have more than one place then we want to show all the markers on the screen. To do this we define two variables (`avg_lat` and `avg_lon`) and use these to total up the coordinates of each place. We then divide each one by the number of places we have to give us a middle point.

This solution works for a city-sized area but if you're planning to have places across multiple cities or even countries then using `fitbounds()` might yield better results. This will require a bit more digging into React as you'll need to tap into the power of hooks (especially [useEffect()](https://reactjs.org/docs/hooks-effect.html)).

<hr/>

### 3.3. Adding props to our Map component

<hr/>

Next, go to `Map.js` in our `app/javascript/components` folder. Here we need to add the ability for our component to access `props`. In the function declaration add the following:

```
const Map = (props) => {
...
```

By passing props in the declaration we now have access to whatever data we feed the component when it's rendered. In our `index.html.erb` we provided `center`, `zoom`, and `markers`, so in our component we can access these by simply adding `props.` before the name. In the body of our Map function, let's make a few changes.

First Let's see what data we're actually working with. Inside the body of our Map component, let's do a quick log to console:

```
const Map = (props) => {
  console.log(props)
  ...
}
```

Which will return this in our browser console:

![Props console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kpvuzywial28d6u7b04c.png)

Think of the parent `Object` as the props in our `props.{data}` call. If we call `props.center` we'll get an array with two elements - our latitude and longitude. They aren't labeled as such but this is what we passed earlier in our controller as `@map_center`.

We can only access props within the body of the function so let's move our `center` variable inside and give it some props:

```
const Map = (props) => {
  const center = {
    lat: props.center[0],
    lng: props.center[1]
  };

  return (
    <GoogleMap
      mapContainerStyle={containerStyle}
      center={center}
      zoom={10}
    >
      { /* Child components, such as markers, info windows, etc. */ }
      <></>
    </GoogleMap>
  )
}
```

Refresh your page and you should see Washington D.C. (or your place of choice):

![Map of Washington D.C. zoom 10](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bj4ceonqzmmd6sxccv4f.png)

We now have a dynamic center point based on the conditions we set out in our index action! Next, let's set some props for our zoom property:

```
<GoogleMap
      mapContainerStyle={containerStyle}
      center={center}
      zoom={props.zoom}
    >
```

Now our map should be more focused on a general area:

![Map of Washington D.C. dynamic zoom](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/05pwvpct4inkco6tiw5m.png)

<hr />

## 4. Making markers for our places

<hr />

Our map is still missing a key part - markers. We have a general idea of where we are thanks to our new default center and zoom, but we have no idea where to look. Here we're going to make use of the Marker and InfoWindow components provided to us by `react-google-maps`. Let's start by importing them at the top of our Map component. Where we import GoogleMap, replace that line with the following:

```
import {
  GoogleMap,
  Marker,
  InfoWindow,
} from '@react-google-maps/api';
```

Next, inside of our `GoogleMap` component within the function let's add our markers. If we revisit the data we logged in the console earlier, you'll see that markers were provided as an array:

![Markers array](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vrzduuudakw9jz3y8bna.png)

This data comes from `@markers` in our controller. A powerful way of creating multiple components easily is to use JavaScript's `.map` method. Inside the `GoogleMap` component:

```
<GoogleMap
  mapContainerStyle={containerStyle}
  center={center}
  zoom={props.zoom}
>
  {props.markers[0].map(marker => 
    <Marker
      key={marker.id}
      animation={2}
      position={{
        lat: marker.lat,
        lng: marker.lng,
      }}
    />
  )}
</GoogleMap>
```

Because `markers` is an array of arrays, we need to select the element at the first index to map over. From there, we create a new `Marker` component for each marker element. 

When creating multiple components from an array we need to provide React with unique keys to differentiate them. In this case we have a built-in `id` that will always be unique thanks to the way our PostgreSQL database works. We then provide the position as a JavaScript object in the same way that we created the `center` variable earlier, only this time it's done in-line.

We should finally have some working markers on our map. Let's check it out. Refresh your page and you should see this:

![Map with markers](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/98wq9xigf0sy102vvywn.png)

Awesome! We now have dynamic markers that will update each time you add or remove a place from your database, along with your maps zoom and center point. All that's left to do is add an `InfoWindow` component to our markers to display some information when we click on them.

<hr />

## 5. Adding infowindows to markers

<hr />

At the moment clicking on our markers yields no results which is terrible for user experience. Instead maybe we'd like to show the name of the place, the address, or any other information we make available.

We have to do a bit of extra work before we can implement these. First, we're going to make use of the [useState](https://reactjs.org/docs/hooks-state.html) hook due to the fact that we've defined Map as a functional component. First, let's import `useState` at the top of our component:

```
import React, { useState } from 'react'
```

The reason we have to use this hook is because we need to be able to determine which marker we've clicked on in order to display the correct infowindow. To do this we'll create a state called `selected`. Inside our Map function, add the following at the top:

```
const Map = (props) => {
  const [selected, setSelected] = useState(null)
  ...
}
```

We'll use this state together with an `onClick` function to tell React which marker we've clicked on. To do this, we need to add another property to the Marker components rendered from our `.map`:

```
{props.markers[0].map(marker => 
  <Marker
    key={marker.id}
    animation={2}
    position={{
      lat: marker.lat,
      lng: marker.lng,
    }}
    onClick={() => {
      setSelected(marker)
    }}
  />
)}
```

Nothing's happening yet because we're not telling React what to do when the state changes. To do so, add this underneath your markers, before the closing tag of the `GoogleMap` component:

```
{selected ? 
  (<InfoWindow 
      position={{ lat: selected.lat, lng: selected.lng }}
      onCloseClick={() => {
        setSelected(null)
      }}
    >
    <div style={{ maxWidth: 120 }}>
      <p>{ selected.name }</p>
      <small>{ selected.address }</small>
    </div>
  </InfoWindow>) : null
}
```

Here we're using a ternary operator to do a bit of conditional rendering for us. If the marker is selected (by clicking on it), it'll render an `InfoWindow` component that shows us the name and address of the chosen place. When we click the close button, it sets the selected state back to `null` in this line:

```
onCloseClick={() => {
  setSelected(null)
}}
```

If the marker doesn't match the selected one, nothing is rendered. If you refresh your page, when you click on any marker you should see a small window appear:

![Marker infowindow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sph9dcbyj4mgzcnj8mx7.png)

<hr />

## 6. Next steps

<hr />

And there you have it, we now have a working map rendering markers and infowindows for each place we create. Where you go from here is entirely up to your imagination - this could be used to display branches of a company, cafes in a city, or tap into a location-based API to display events in an area.

If you plan to host your project online, make sure that you import your environment variables into whichever platform you choose as these will replace the data in the `.env` file.

Almost every aspect of the map can be customised, from [the color of the map](https://snazzymaps.com/) to the marker icons and infowindow contents like these from my final project at Le Wagon:

![Safe Spaces infowindow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sr8pjmk36ur6100hqg6d.png)

Feel free to clone the project and make it your own and adapt it. Share your thoughts and ideas in the comments!

Thank you for following along!

<hr />

## 7. Get in touch

If you want to see more of my work, feel free to reach out through any of these platforms:

* [Portfolio](https://www.rhysmalyon.com)
* [GitHub](https://www.github.com/RhysMalyon)
* [LinkedIn](https://www.linkedin.com/in/rhysmalyon)