Posts

, , ,

Google Maps in React / Redux: Interactivity Across Different Components

Rich UIs provide users with a variety of ways to interact. For example, HipCamp, a platform for discovering camping opportunities, uses a combination of a map-based UI and an informative list of campgrounds:

hipcamp.png

This split UI gives users an idea of location through identifying the campground on a map, while keeping the map tidy by using the marker info windows as basic identifier, keeping the more verbose content within the campground list.

Explore React Courses

In this post we will build on our previous learnings in list filtering through state management in React/Redux by adding a map to the UI. We will see how to use state to link the list and map views together, enabling the user to leverage the information of both UI elements when interacting with one or the other. We will also extend the filtering functionality to the markers by connecting the map components to the same state elements as the filter components. The resulting app will filter both the markers and the campground list, and provide users with visual cues when interacting with one or the other. Clicking on a marker will pop up an info window showing the campground name and also highlight the corresponding list item by changing the border. Likewise, clicking a list item will open the info window for the corresponding marker and change the item border:

map.png

You can get the code for this app on GitHub and interact with it live at this Heroku demo app. We will continue using the google-maps-react npm module and some of the techniques outlined on Full Stack React for working with this package. All of our state management will be done via Redux, as opposed to at the component level in the Full Stack React article.

Adding the Map

Picking up where we left off in the previous post, we will add a google maps container to CampFilterApp.jsx:

render() {

 return (

 <div className="container">

 <Jumbotron>

 <h1>
 Crater Lake Camping
 </h1>

 </Jumbotron>

 <br></br>

 <CampFilterList {...this.props }/>

 <br></br>

 <CampMapContainer {...this.props }/>

 <br></br>

 <CampList {...this.props }/>

 </div>

 )
};

The CampMapContainer component will serve as a wrapper for the google maps components, and a point of entry for the GoogleApiComponent provided by google-maps-react. First lets look at the render method for CampMapContainer.jsx:

 

render() {

 return (

 <div>

 <CampMap google={this.props.google}>

 {this.props.markers.map(marker =>

 <Marker

 key={marker.get('title') }

 title={marker.get('title') }

 description={marker.get('description') }

 properties={marker.get('properties') }

 position={marker.get('position') }

 mapOn={marker.get('mapOn') }

 addMarker={this.props.addMarker}

 onMarkerClick={this.props.onMarkerClick}/>

 ) }

 <InfoWindow {...this.props}

 marker={this.props.activeMarker}

 visible={this.props.showingInfoWindow}>

 <div>

 <h4>{this.props.selectedTitle}</h4>

 </div>

 </InfoWindow>

 <CampList {...this.props}/>

</div>

 ) }

We have four components inside the container – CampMap is a container for the map itself, which contains a marker component for each marker in our markers property and an InfoWindow component to be set based on the activeMarker property. We will discuss these components more in detail in a bit.

After the class definition in CampMapContainer.jsx, we will add an export for the google maps component:

let key = config.getGoogleKey()

export default GoogleApiComponent({

apiKey: key

})(CampMapContainer)

You can use the API key included in the config supplied with the config.js file for demonstration purposes, but please don’t abuse it! You can use the google-maps-react package without an API key, but by providing one we receive a google property which can be passed to the components to create markers, info windows, and maps as we will see shortly. Notice that the <CampMap> component in the render method above makes use of the google property.

Lets dive into the Google Maps components.

The CampMap Component

This component sets up our map and renders the Marker components. In the render method, we set the min size for the map and call the renderChildren function. You can check out that code in CampMap.jsx. Essentially, it will render any children of the map object, in our case the Marker components, passing on the google and map properties.

render() {

 const style = {

 minWidth: '400px',

 minHeight: '400px'

 }

 return (

 <div className="row">

 <div style={style} ref='map'>

 {this.renderChildren() }

 Loading map...

 </div>

 </div>

 )

}

Recall above that we passed the google property to this component. The API connection is asynchronous, meaning that initially this property will be undefined. When the API returns, the google property will be updated, resulting in a ComponentDidUpdate event on the CampMap component. This has two impacts:

  1. When the render method is initially called, and the Markers are rendered as a result, the map property passed to the Markers in renderChildren will be undefined. This means the markers will not be attached to the map since it does not yet exist. In order to attach the markers to the map we will need to renderChildren again after the API returns.
  2. We need to wait until the API returns a valid google property to load the map.

To account for this, we will handle the ComponentDidUpdate event in the CampMap component, calling both the loadMap function that draws the google map and forceUpdate, which will force the component to re-render including the call to renderChildren. We will only do this if the google property has been updated, to avoid re-rendering for other cases where componentDidUpdate is fired. The loadMap function sets the map parameters and attaches it to the DOM, see CampMap.jsx.

componentDidUpdate(prevProps, prevState) {

 if (prevProps.google !== this.props.google) {

 this.loadMap();

 this.forceUpdate()

 }

}

The Marker Component

Now that we have our map created, let’s take a look at the Marker component, which receives the google and map properties from its parent component, CampMap, in addition to these properties defined in CampMapContainer:

<Marker

key={marker.get('title')}

title={marker.get('title')}

description={marker.get('description')}

properties={marker.get('properties')}

position={marker.get('position')}

mapOn={marker.get('mapOn')}

addMarker={this.props.addMarker}

onMarkerClick={this.props.onMarkerClick}/>

Because google maps renders components directly to the DOM, the render method of this component simply returns null. Instead, we render the marker when there is a change in the map property or in the marker properties or mapOn properties. We are only looking at changes in these specific marker properties because the others are static in our application. See the ComponentDidUpdate method in Marker.jsx

You’ll notice that we are passing two actions down to the Marker component; addMarker and onMarkerClick. The addMarker action is used to create a list of google marker objects as part of the Redux state. This marker array will be used to allow interactivity between the markers and the campground list items. The onMarkerClick action will be used to set the info window content. We will discuss these actions more in detail later on. The InfoWindow component implementation is from the Full Stack React Google Maps article, which you can reference to understand the lifecycle of this component.

The CampListItem Component

We need to make a few changes to our CampListItem component from the last post to change the border of the selected element and enable opening the corresponding marker’s info window. Let’s take a look at the image in the CampListItem component render method:

<img src={img_url} alt="campground" ref="cg_image" style={{width:200, height:100}} onClick={() =>this.props.onMarkerClick(this.getMarker(this.props.title))}></img>

We’ve added a ref property to the image which we will use to set the border style when the item is selected. We’ve also added a click handler to call onMarkerClick with the current marker. This is the same onMarkerClick called by the Marker component, which will set the info window content and open/close it as required. Notice that we are calling a local function getMarker to retrieve the current marker for the onMarkerClick parameter. First let’s take a look at setting the style with the ref property in CampListItem.jsx:

componentDidUpdate(prevProps, prevState) {

if (this.props.activeMarker !== prevProps.activeMarker) {

let img_ref = this.refs.cg_image

if (this.props.showingInfoWindow && (this.props.selectedTitle === this.props.title)) {

img_ref.style.border = "3px solid black"

}

else {

img_ref.style.border = null

}

}

}

On ComponentDidUpdate we check to see if the activeMarker field has changed, and if so we proceed with changing the image border. Next, we will either add the border if this item is selected, or remove it if it is no longer selected. If it turns out that the info window should be shown and the selectedTitle property matches the title of the current marker we will update the border to “3px solid black”, otherwise we set the border to null.

Recall that we have been using addMarker to create an array of google maps markers as part of our Redux state whenever a new marker is rendered. We now have two arrays of markers as part of our state, from index.js:

function set_state(campgrounds) {

 store.dispatch({

 type: 'SET_STATE',

 state: {

...

 markers: campgrounds,

 gmapMarkers: [],

 ...

}

})

}

The markers array is our geoJSON-based markers used for filtering and displaying data. The gmapMarkers array is for the google marker objects we need for the map markers. Both sets of markers have a title field that can be used to link the two. In the CampListItem component, the marker property is from the markers array. The onMarkerClick action needs a marker from the gmapMarkers array in order to attach the info window to the correct marker, so we have a helper function getMarker that retreives the matching gmapMarkers element to provide to onMarkerClick:

getMarker(title_match) {

 let match_list = this.props.gmapMarkers.filter(item =>

 item.get('title') === title_match

 )

 if (match_list) {

 return match_list.first()

 }

 else {

 return null;

 }

}

Where this.props.title is provided as the title_match parameter.

Now that we’ve discussed the component hierarchies, let’s take a look at how to hook up these actions in Redux.

Redux Implementation

Remember we first need to add our new actions to the action_creators.js file, which you can review here. Next, we add the following to our reducer.js file to handle these new actions:

function onMarkerClick(state, marker) {

 return state.merge(Map({

 'activeMarker': marker,

 'selectedTitle': marker.get('title'),

 'showingInfoWindow': true

 }))

}

function addMarker(state, marker) {

 let markers = state.get('gmapMarkers')

 let newMarkers = markers.push(marker)

 return state.update('gmapMarkers', oldmarkers => newMarkers)

}

Whenever the markerOnClick event is fired, we want to show the info window, so showInfoWindow is set to true. activemarker and selectedTitle are set based on the marker passed up from a CampListItem or Marker component. For addMarker, we simply add the new marker to the existing gmapMarkers array. Don’t forget to add the new actions to the reducer:

export default function (state = Map(), action) {

 switch (action.type) {

 case 'SET_STATE':

 return setState(state, action.state);

 case 'CHANGE_FILTER':

 return changeFilter(state, action.filter);

 case 'MARKER_CLICK':

 return onMarkerClick(state, action.marker)

 case 'ADD_MARKER':

 return addMarker(state, action.marker)

 default:

 return state

 }

}

One last change to make in the reducer, and this is for the changeFilter action. Recall that we used a mapOn property in the Marker component:

if (!this.props.mapOn) {

 this.marker.setMap(null);

}

else {

 this.marker.setMap(map)

}

The mapOn property was added to the geoJSON-based markers array to set marker visibility based on the filter settings. Adding the following to changeFilter in reducer.js will set mapOn to true of the marker should be shown:

let markers = state.get('markers')

let updatedMarkers = markers

markers.forEach(marker => {

 let markerIndex = getMarkerIndex(state, marker.get('title'))

 let mapOn = true

 active_filters.forEach(item => {

 if (marker.get('properties').get(item.get('id')) !== true) {

 mapOn = false

 }

 })

Based on the mapOn property we can “remove” the markers not to be displayed based on the current filter settings by marker.setMap(null) if mapOn is false.

And that’s a wrap! Using the gmapMarkers property and invoking the onMarkerClick action from both the Marker and CampListItem components we have enabled opening of the InfoWindow from clicking on the CampListItem image and highlighting the CampListItem image as a result of clicking on the Marker. The additional field mapOn enables us to filter both the campground list and the markers.

,

‘Find Your Park’ with ReactJS, Google Maps and the DarkSky API

This year marks the 100th anniversary of the National Park Service, an organization established by Congress for “preserving the ecological and historical integrity of the places entrusted to its management while also making them available and accessible for public use and enjoyment”. Among the centennial festivities is a call to get out to experience the National Parks and “Find Your Park.”

We thought it might be fun to help you plan a National Park visit with this React app that provides current forecast information for the National Parks.

See the Pen National Park Weather with React by Sev (@sevleonard) on CodePen.

In this post we will use the Google Maps API, the DarkSky API for weather forecasting, and the Recreation Information Database (RIDB) for National Parks info. We’ll wrap all this up in a React app to help you plan a National Parks trip. For more info on getting started with RIDB and Google Maps check out Interactive Mapping with Python, GeoJSON, and JavaScript and Advanced Google Maps with JavaScript. If you are coming to React from a background in jQuery this primer is a great way to get started.

In this post you will learn:

  • Some quick Python data wrangling to acquire, clean, and organize data from an API
  • How to create a Google Maps object in React, import markers from JSON, and update info windows
  • Using state properties to optimize forecast retrieval
  • Using axiom to load external data in React
  • Using Promises to encapsulate calls to the DarkSky API
  • Dealing with CORS issues in codepen

We’re covering a lot of ground in this post, so if you’re here for something specifc you might want to skip to that area:

Using Python to get National Park Data
Setting up Google Maps in React
Integrating the DarkSky API
Async Marker Retrieval
Forecast Promises
Getting the forecast: state or API?

Getting Information on National Parks


Using the RIDB organizations endpoint we can get a list of all properties managed by the National Park Service. We’ll use a bit of Python to get this info from the API and store it into a flat file for use by our React app later on. You will need to get a RIDB API key to execute this code.

endpoint = 'https://ridb.recreation.gov/api/v1/organizations/{orgID}/recareas'
# NPS is org_id 128
org_id = 128

offset=0
params = dict(apiKey= config.RIDB_API_KEY, offset=offset)
nps_url = endpoint.replace('{orgID}', str(org_id))
resp = requests.get(url=nps_url, params=params)
data = json.loads(resp.text)
df = json_normalize(data['RECDATA'])

The RIDB endpoint will return 50 records at a time. To see how many records there are for the NPS org, we can look at the TOTAL_RECORDS field in the response data:

max_records = data['METADATA']['RESULTS']['TOTAL_COUNT']

Max records is 590. There are 58 National Parks, so clearly we are getting a lot more information than we need. We’ll extract the National Parks after we download the entire list.

We’ll continue calling the API until we’ve reached max_records, adding the response data to a data frame to accumulate all the organizations. We will use the offset value to keep track of where we are in the list and update the API query to provide the next 50 records

#save the first 50 records before we iterate

df_nps = df

while offset < max_records: offset = offset + len(df) print("offset: " + str(offset)) df = pd.DataFrame() params = dict(apiKey = config.RIDB_API_KEY, offset = offset) try: resp = requests.get(url = nps_url, params = params) except Exception as ex: print(ex) break
if resp.status_code == 200: data = json.loads(resp.text) if data['METADATA']['RESULTS']['CURRENT_COUNT'] > 0:
df = json_normalize(data['RECDATA'])
df_nps = df_nps.append(df)
else :
print("Response: " + str(resp.status_code))

Now lets look for the National Parks among this list. Lets start out by just checking the name for “National Park”

df_np = df_nps[df_nps['RecAreaName'].apply(lambda x: x.find('National Park') > 0)]
df_np.shape
(61,18)

That certainly helped! Looks like we have a few extras above the 58 National Parks we have to weed out. Another thing to check for is latitude and longitude values, since we want to plot this data on a map. Lets see how many of these 61 areas are missing location info:

df_np[df_np['RecAreaLongitude'] == ""].RecAreaName
21 Arches National Park
38 Black Canyon Of The Gunnison National Park
18 Carlsbad Caverns National Park
47 Denali National Park & Preserve
43 Gates Of The Arctic National Park & Preserve
49 Glacier National Park
16 Guadalupe Mountains National Park
21 Haleakalā National Park
43 Katmai National Park & Preserve
9 Lassen Volcanic National Park
21 Western Arctic National Parklands
9 Congaree National Park Wilderness
48 Rocky Mountain National Park Wilderness

13 of the National Park areas are missing location info. Looking at the last three in the list, Western Arctic National Parklands, Congaree National Park Wilderness, and Rocky Mountain National Park Wilderness are not part of the 58 National Parks, so we will drop them from the list. For the other 10 we can look up the latitude/longitude and update the data frame with those missing values using pandas update:

# file we stored the missing lat/long values in after looking them up
missing_latlongs = pd.read_csv('missing_lat_longs.csv')
missing_latlongs.head()
RecAreaName RecAreaLatitude RecAreaLongitude
0 Arches National Park 38.7331 -109.5925
1 Black Canyon Of The Gunnison National Park 38.5754 -107.7416
2 Carlsbad Caverns National Park 32.1479 -104.5567
3 Denali National Park & Preserve 63.1148 -151.1926
4 Gates Of The Arctic National Park & Preserve 67.9204 -153.2753

#set the indexes for missing_latlongs and df_np to the RecAreaName. The update function uses the index to merge the data
missing_latlongs = missing_latlongs.set_index('RecAreaName')
df_np = df_np.set_index('RecAreaName')
df_np.update(missing_latlongs)

# RecAreaName has been converted to an index, recreate this field and reset the index
df_np['RecAreaName'] = df_np.index
df_np['newIndex'] = range(0,58)
df_np.set_index(df_np['newIndex'])
df_np = df_np.drop('newIndex', axis=1)

# check that we captured all 58 National Parks
df_np.shape
(58, 18)

#save to csv for later use
df_np.to_csv('np_info.csv')

Great! We now have all 58 National Parks and their lat/long data for the map. Next lets switch over to React to create the Google Maps object for displaying these parks.

Creating the Google Maps object


While we are able to use the Google Maps API with React, it doesn’t exactly let us do so in a way that is truly React-ish, you can read more about that issue here. That caveat aside, lets dive into creating a map in React.

We’ll start by creating a React Component object to encapsulate the methods, states, and properties for the map

class NpsForecastMap extends React.Component 

The Component class includes a number of lifecycle methods to handle creation, initialization, interaction, and disposal. Lets get started by defining some properties

  state = { 
    forecast:{} 
  };

  propTypes = {
    np_url: React.PropTypes.string,
    init_lat: React.PropTypes.number,
    init_lng: React.PropTypes.number
  };

Our app is going to generate the current forecast for each National Park when a user clicks on the marker. Since the DarkSky API isn’t free, we can keep track of if the forecast has been fetched already to avoid the overhead, both financially and in waiting on the API response, by maintaining a dictionary of forecast state. We’ll see later how this will be used to cache the current forecast.

The propTypes objects enables us to validate the inputs to our component. By parameterizing the input geojson file and the initial map center we can reuse this component with other datasets, for example, local state parks or a set of points you plan to visit on a road trip. The React.PropTypes validators enable us to confirm that the inputs received are of the expected type, though note that these validators will not screen for a valid geojson file or valid latitude and longitude, they simply check the type of the object passed.

Now that we have setup the component properties, lets take a look at some of the methods for our NpsForecastMap component.

The render() method is required, and will read the state and props properties of the component. This method will also contain the HTML representation of our component to be added to the DOM at the time of rendering. For the NpsForecastMap component will simply add a div for React to reference when building the map, as we will see later:

render() {
    return

}

Remember from previous articles that we need to give the map divs a defined size:

.NpsForecastMap {
height: 100%;
position: absolute;
width: 100%;
}

In the HTML we have a div available for React to render the component in:

<div id="container"></div>

At the end of our React App file, we will attach the Map component to the div “container”.

ReactDOM.render(, document.getElementById('container'));

After the NpsForecastMap component has been rendered we can initialize the Google Maps object in the ComponentDidMount method, which runs after the initial rendering has occurred:

componentDidMount() {
    this.map = this.createMap()
    ...
  }
  
createMap() {
    let mapOptions = {
      zoom: 3,
      center: this.mapCenter()
    }
    return new google.maps.Map(this.refs.mapdiv, mapOptions)
  }

mapCenter() {
    return new google.maps.LatLng(
      this.props.init_lat,
      this.props.init_lng
    )
  }

Notice that we are creating the map by referencing the mapdiv ref we created in the render() method. By providing references to entities with in the NpsForecastMap component we can avoid having to directly reference the DOM.

Async Marker Retrieval


In React apps, we want to leave the DOM rendering to React as much as possible, meaning JQuery’s methods of direct DOM manipulation don’t play nice with the React paradigm. For async calls to APIs, we can use the axios promise-based HTTP client in place of AJAX. Lets take a look at how to do that for loading the National Park geojson file.

After creating the map in the componentDidMount method, we will call a method to load the geojson and create the makers for the map.

componentDidMount() {
    this.map = this.createMap()
    this.loadFeatures()

Taking a look at the first part of the loadFeatures function, we can see how similar the axios API is to JQuery for getting data:

  loadFeatures() {
    ...
    var self = this
    axios.get(th.props.nps_source)
        .then(function(result) {   
           ...

Notice that we are saving the context ‘this’ off to a local variable ‘self’. When working with async calls its important to remember that we will be operating in a different scope when the callback occurs, so ‘this’ will pertain to that local scope, not the scope in which we called loadFeatures. Creating a variable to encapsulate the initial scope will enable us to refer to methods and properties of the class when handling the result from the async call.

When we get the result from the axios get call, we’ll iterate through each feature in the collection and create the markers:

axios.get(th.props.nps_source)
        .then(function(result) {   
           for (let val of result.data.features) {
             let marker = self.createMarker(val, self.map)

Notice that we are using the ‘self’ variable to access the createMarker method of the NpsForecastMap component and to pass the map object to createMarker.

  createMarker(val, map) {
     let pointval = new google.maps.LatLng(
       parseFloat(val['geometry']['coordinates'][1]),
       parseFloat(val['geometry']['coordinates'][0]));
     let titleText = val['properties']['title']       
     let marker = new google.maps.Marker({
       position: pointval,
       map: map,
       title: titleText
     });
    
    return marker
  }

Once we parse the geojson we can create a new Marker object with the features, using the ‘map’ parameter to link the marker to the NpsForecastMap property. Now the marker will display on the map. We’ll come back to the markers to add an InfoWindow with the park name and current weather, for now lets take a look at how to get our forecast from DarkSky

See the Pen National Park Weather with React by Sev (@sevleonard) on CodePen.

Integrating the DarkSky API

Ideally, we would separate out the call to the forecast into a separate react component, ForecastAction.js for example. This would enable us to be flexible with our forecast provider, and provide a more appropriate separation of view and control in our app. We’ve started that work by creating a function with a fairly universal interface, taking only latitude and longitude as parameters and returning the current weather:

getForecast(lat, lng) {
    ...
    let request_url="https://crossorigin.me/https://api.darksky.net/forecast/8266ff95ef9bbfccf0ea24c325818f31/"

    request_url = request_url +  lat + "," + lng 
    axios.get(request_url)
        .then(function(result) {
            let content =  result.data.daily.summary

To access the DarkSky API from codepen, we need to use crossorigin.me to avoid CORS issues when querying the forecast. To do this we prepend crossorigin.me to the DarkSky API URL. Using the axios library again we query the API for our location and extract the daily summary, which will look something like:

“Mixed precipitation throughout the week, with temperatures peaking at 55°F on Saturday.”

So we have our forecast, but we have an async call within getForecast. We want to add the daily summary text to our marker info window, which needs to happen after we have retrieved the forecast. One option would be to pass the marker info window into the getForecast function, so we could directly update the info window when we get the forecast. But then we are mixing view elements with control elements, and making the call to getForecast less generic to enable swapping in a different forecast provider. Instead, we can wrap getForecast in a Promise, preserving the separation of control from view and maintaining the generic interface of the function.

Forecast Promises


Appropriate, since a forecast is kind of like a promise about the weather, your mileage may vary as to how well weather promises are kept, however.

If you are new to Promises here is an overview of the Promise object and how it works. At a high level, we will be creating a structure within the getForecast function that will enable us to call it like the axios get function, i.e.

getForecast(lat,lng)
    .then(function(result) {
        // add forecast to info window
    })

First, we need to create a promise object, forecast_p. The forecast object needs to implement a resolve directive, for providing direction on when the async method has completed successfully, and a reject method for when things don’t turn out as we expect. In this promise, we only have a resolve directive for brevity.

 getForecast(lat, lng) {
    let forecast_p = new Promise(
      function(resolve,reject) {
        ...
         axios.get(request_url)
          .then(function(result) {
            let content =  result.data.daily.summary
            resolve(content)
         }); 
      });

After we get the forecast from DarkSky we resolve with the daily summary.

With the Promise created, we can now determine what to do after the forecast has been fetched:

forecast_p.then(
      function(result) {
        return result
      }
     )

return forecast_p

The result will be the daily forecast we passed to resolve() when creating the Promise. At this point, we return the Promise so the caller can determine when to initiate the DarkSky request.

Getting the forecast: state or API?


Now that we have a mechanism for getting the forecast, we can proceed with adding a click handler to the marker. In addition to getting the current forecast (or not, depending on the state) we will pop up an InfoWindow to display the park name and forecast summary. On click, we will close the current InfoWindow, this will avoid multiple InfoWindows being created and requiring the user to manually close them. We are also creating a local variable getWeather which will be used to determine if we can read from our forecast state property or if we need to query DarkSky.

let marker = th.createMarker(val, th.map)
marker.addListener('click', function() {
    infoWindow.close()
    let title = this.title
    let infoContent = ""
    let getWeather = false-

Now, lets check our forecast state to see if we already have the weather for this marker:

// check to see if we already have the forecast for this marker
if (title in th.state.forecast) {
    infoContent = title + "
" + th.state.forecast[title]  
}
else  {
    infoContent = title + "
Loading Current Weather..."
    getWeather = true
}

Our forecast state property is indexed by park name, which is the marker title. If we can find the park in the forecast state property we can read the forecast directly from there without having the query the API. We can create the info window content directly from here.

Otherwise, we will provide a message ‘Loading current weather’ and set our getWeather variable. Based on this, we can call into our getForecast Promise, updating the info window with the result:

if (getWeather) {
    th.getForecast(marker.position.lat(),marker.position.lng())
    .then(function(result) {
        infoWindow.setContent(title + "
" + result)
        th.state.forecast[title] = result
    })
}

Check it out for yourself! There are some debug messages you can check out in the console to verify if the weather is being read from state

See the Pen National Park Weather with React by Sev (@sevleonard) on CodePen.

Summary

Wow, we’ve covered a lot here. Pat yourself on the back and check out this React google maps module if you’d like to take a shortcut. Tom does a great job of React-ifying the Google Maps API in this npm module.

When you’re ready for more, consider these potential next steps

  • Add an option for the user to specify a date range to check the weather for. DarkSky has a nice historical API you can use to help plan trips ahead of time
  • Separate out the forecast and maps elements into different components for better MVC separation. Check out Tom’s repo to see how he handles references to the map object for the Marker component.
  • Instead of using Python, create another React component for getting and cleaning the National Parks data, which you can then pass as a property to the NpsForecastMap component.