Posts

Let’s face it, while it’s fun to write applications from scratch, a lot of software development work involves working with existing code. In this post, we are going to learn how to add a feature to an existing React/Redux app. Following on our previous articles in <list filtering with react/redux> and <component interactivity with react/redux>, we will add a feature to get a weather forecast estimate to help us figure out when is the best time to visit crater lake. In addition, since we are querying an API, we will be learning how to use async actions to retrieve and display the weather.

Here’s an outline of the process we will be following

  1. Adding a date selector component to let the user pick a date for getting the weather.
  2. Getting weather information from the <DarkSky api>
  3. Identifying where and when to update state to incorporate our new weather feature
  4. Handling state changes with async interfaces

Check out the final product on Heroku and get the code on Github.

finished image.png

Adding a date selector

Using the react-datepicker module we’ll create a new WeatherDatePicker component to show the date picker and the forecast results. When the user loads the page they will see “Select a date…”:

date picker.png

After a date is selected, the forecast will be displayed:

date picked.png

Taking a look at the render method in WeatherDatePicker.jsx:

render() {

 return (

 <div>

 Travel Date:

 <DatePicker

 placeholderText="Select a date..."

 selected={this.props.selectedDate}

 dateFormat = "YYYY-MM-DD"

 onChange={(selectedDate) => {

 let newDate = this.checkDate(selectedDate)

 if (newDate) {

 this.props.fetchWeather(this.checkDate(selectedDate), this.props.currentLat, this.props.currentLong)

 }

 } }/>

 Forecast: {this.props.weatherSummary}

 </div>

 );

}

When the DatePicker is opened and a date is selected, firing the onChange event, we first check the date to make sure it is in the future:

checkDate(selectedDate) {

 if (selectedDate.isBefore(this.props.currentDate)) {

 alert("Please pick a date in the future.")

 return undefined

 }

 else {

 return selectedDate.format("YYYY-MM-DD")

 }

}

The DatePicker module uses moment.js for the date object. We can use moment’s isBefore method to compare two moment objects, and display an alert if the user picks a date in the past. If the render method receives a valid result, it calls the delegate fetchWeather, passing along the selected date, to retrieve the weatherSummary.

Now we will add the WeatherDatePicker component and the DarkSky attribution graphic to CampFilterApp.jsx:

render() {

 return (

 <div className="container">

 <div><a href="https://darksky.net/poweredby/"><img src="https://darksky.net/dev/img/attribution/poweredby.png" style={{ width: 100 }}/></a></div>

 <Jumbotron>

 <h1>Crater Lake Camping</h1>

 </Jumbotron>

 <br></br>

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

 <br></br>

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

 <br></br>

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

 </div>

 )
};

While we are working with components, let’s also update the Redux store to include the thunk middleware that we’ll be using for the next steps. We just need to add thunkMiddleware to our createStore call. In index.js:

import thunkMiddleware from 'redux-thunk'

const store = createStore(reducer, applyMiddleware(

 thunkMiddleware

))

Adding state and async actions for the weather feature

Now that we have a component to display the weather, lets look into the associated actions and state needed to do the work.

For the weather feature, the following fields are added to the state in index.js and set as props in CampFilterApp.jsx:

currentDate: today,

weatherSummary: "",

currentLat: 42.9456,

currentLong: -122.2,

selectedDate: undefined

The DarkSky API needs a position and date to retrieve the weather. Since our app is localized to Crater Lake we’ll use those coordinates to retrieve the weather. Don’t forget to convert these new state variables to props to send down to the WeatherDatePicker component in CampFilterApp.jsx:

function mapStateToProps(state) {

 return {

...

 weatherSummary: state.get('weatherSummary'),

 currentDate: state.get('currentDate'),

 currentLat: state.get('currentLat'),

 currentLong: state.get('currentLong'),

 selectedDate: state.get('selectedDate')

 };

}

To query the API and update the selectedDate and weatherSummary variables we will use thunk middleware and fetch as described in this article from the Redux docs.

To handle an async action, we will split the API call into request and receive actions. Our request action, REQ_WEATHER, will update the selectedDate to the value passed from the DatePicker and weatherSummary to “Loading…” to let the user know the weather info is loading. From action_creators.js:

export function reqWeather(weatherDate) {

 return {

 type: 'REQ_WEATHER',

 weatherDate

 }

}

And the corresponding function in reducer.js:

function reqWeather(state, weatherDate) {

 return state.merge(Map({

 'weatherSummary': "Loading...",

 'selectedDate': weatherDate

 }))

}

For the receive action, RECV_WEATHER, we add a response field to our state to hold the JSON returned by the DarkSky API:

export function recvWeather(weatherDate, result) {

return {

type: 'RECV_WEATHER',

weatherDate,

response: result

}

}

The corresponding method in reducer.js for parsing the API response into the weatherSummary field:

function recvWeather(state, weatherDate, result) {

 let content = ""

 try {

 let tempMax = result.daily.data[0].temperatureMax

 let tempMin = result.daily.data[0].temperatureMin

 let summary = result.daily.data[0].summary

 content = summary + " High: " + Math.ceil(tempMax) + " Low: " + Math.ceil(tempMin)

 }

 catch (err) {

 console.log("couldnt get weather summary: " + err)

 }

 return state.merge(Map({

 'weatherSummary': content

 }))

Alright, at this point you may be wondering “This is great and all, but where’s the part where we actually query the API?” Excellent question! That’s next.

Now that we have our request and receive actions setup, lets combine them in the fetchWeather action, which if you recall, is what is getting called from the WeatherDatePicker onChange event:

export function fetchWeather(weatherDate, currentLat, currentLong) {

 let request_url = "https://crossorigin.me/https://api.darksky.net/forecast/8266ff95ef9bbfccf0ea24c325818f31/"

 let weather_str = weatherDate.format("YYYY-MM-DD") + "T00:00:00"

 request_url = request_url + currentLat + "," + currentLong + "," + weather_str

 return function (dispatch) {

 dispatch(reqWeather(weatherDate))

 return fetch(request_url)

 .then(response => response.json())

 .catch(error => {

 console.log("unable to get weather " + error)

 })

 .then(respData => {

 dispatch(recvWeather(weatherDate, respData))

 })

 .catch(error => {

 console.log("unable to parse weather result " + error)

 })

 }

}

Stepping through this piece by piece – first we are constructing the URL for the DarkSky request. As explained in the Redux article, this is where the thunk middleware comes into play; this is what allows the fetchWeather action to return a function instead of an object.

The first thing the returned function from fetchWeather does is dispatch the reqWeather action to update the UI showing that we are requesting the weather. It then fetches the response from the DarkSky API, dispatching the recvWeather action if it receives a valid result.

Now, we have all the plumbing hooked up to retrieve the weather based on the date chosen in the date picker. Give it a try and download the code. If you want to make updates and deploy your own Heroku app make sure to link the create-react-app buildpack in Settings.

Some next steps to try:

  • Allow the user to get customized forecasts for each campground by modifying the CampListItem component to fetch the weather, as opposed to retrieving a general area forecast as we did in this post.
  • When querying the DarkSky API instead of just getting a daily forecast, look for a week before and a week after and get an average, high, and low temp to give a better idea of seasonal weather patterns

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.