Posts

, ,

Build a Camping Weather App with React, Redux and Thunk Middleware

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
, ,

List Filtering through State Management: Moving from jQuery to React/Redux

Most web applications give people an interface to interact with and explore data. Thus, a core challenge of building applications is continuously and reliably syncing the data with the user interface. Toggled switches should stay toggled. Checked boxes should stay checked. Pushing the ‘like’ button should change that button to ‘liked’. The right data should be showing at the right time. This all fits into the realm of managing ‘state’.

In simple apps, vanilla JavaScript or jQuery generally work fine for managing state. But as applications grow in size and complexity, JS/jQuery can turn into spaghetti code. It will become much harder to know which code is affecting which part of the state. This can create bugs for users and suffering for the developers who have to fix those bugs.

JavaScript libraries and frameworks have been evolving quickly to make applications more reliable and easy to debug. This state management problem is getting easier. React and Redux are two libraries (often used together) that have gained a lot of use in the last 2 years. This tutorial will cover the process of converting a jQuery app to React/Redux.

Explore React Courses

In our prior post about using JQuery and Google Maps to display and filter campgrounds, we learned how to leverage GeoJSON properties to control which markers are displayed on the map, allowing us to see only a subset of campgrounds that meet our criteria.

screen-shot-2017-01-06-at-5-24-00-pm

While this is a fine way to design front end filtering, we can improve upon it using React and Redux to help us make our implementation more DRY than WET. In this post we will learn how to build a campground filtering interface using React/Redux by exploring how this architecture can improve on our prior JQuery implementation. Here are some benefits of making the switch:

  • To add a new filter option in our JQuery implementation, we would have to manually add a new filter button to the UI and hook up event handlers. With React/Redux, we can define the filters we want at runtime and let React/Redux handle the UI and event updates for us.
  • By using state we can avoid having to query the DOM or maintain global variables to keep track of what filters are currently in use
  • React will only redraw elements of the UI which have been altered by changes in state, resulting in faster rendering.
  • Using React components, we can compartmentalize different parts of the UI into separate classes, enabling us to easily update and add new components without having to touch the entire codebase.

In this post we will learn how to create the filtering used in this demo app for finding campgrounds near Crater Lake using React, Redux, and react-bootstrap (for simple styling). You can see the complete code on Github.

screen-shot-2017-01-06-at-5-25-20-pm

The React Components

Lets get started by going over the React components we will use to filter campgrounds. Our top level component, CampFilterList, will create items of type CampFilter from a list of filters passed down via props. Taking a look at CampFilterList.jsx:

render() {

   return (

     <div className="row">

       <div className="col-sm-4">Campground Filters:</div>

       {this.props.filters.map(item =>

         <CampFilter id={item.get('id')}

                 key={item.get('id')}

                 changeFilter={this.props.changeFilter}

                 />

       )}

     </div>

 )}

For each item in the filters list we create a CampFilter component with the id that will be passed back to the changeFilter method, which we will discuss in the Redux section. Taking a look at the CampFilter component, we can see how the changeFilter method is called on click, and the id sent as its parameter.

render() {

return (

<div className="col-sm-2">

<input type="checkbox"

className="toggle"

id={this.props.id}

defaultChecked={this.props.inuse}

onClick={() => this.props.changeFilter(this.props.id)}/>

&nbsp;

<label ref="text">{this.props.id}</label>

</div>

)

}

When a filter is checked, the changeFilter method is called back through the store to the reducer, changing the inuse field, resulting in the campgrounds list being filtered. Like the filters the campgrounds components are list/item, but don’t have any user interaction. You can check out the details in CampList.jsx and CampListItem.jsx.

Setting up the Redux store

Now that we have our filtering React components, let’s go over how we will use Redux to manage state and do the campground filtering.

First, we need to define the initial state for our app in index.js:

function set_state(markers) {

store.dispatch ({

type: 'SET_STATE',

state: {

filters: [

{id: 'shower', inuse: false },

{id: 'pets', inuse: false },

{id: 'flush', inuse: false },

{id: 'water', inuse: false }

],

campgrounds: campground_list

}

})

}

In the set_state function we define a list of filters to be used for our app.  The “inuse” field will keep track of how the user is filtering items in the UI. We also define a list of campgrounds that we will apply the filters to. The campgrounds are read from a geoJSON file with properties relating back to the filters:

"type": "Feature",

"geometry": {

"type": "Point",

"coordinates": [-122.166149, 42.865508]

},

"properties": {

"flush": true,

"shower": true,

"pets": true,

"water": true,

"description": "Flush toilet, Shower",

"title": "Mazama",

"image": "mazama.jpg",

"url": "https://www.craterlakelodges.com/lodging/mazama-village-campground/",

"marker-size": "small"

You can see how the properties map back to the filter IDs, for example, the ‘pets’ property in the geoJSON corresponds to the filter with ID ‘pets’.

The store variable in the set_state function refers to the Redux store, which will handle the state updates for our app. To create a Redux store we need to have a reducer to handle state transition. Let’s add the SET_STATE action to the reducer in reducer.js, enabling us to execute the above code:

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

switch (action.type) {

case 'SET_STATE':

return setState(state, action.state);

default:

return state

}

}

We are using the immutable.js library, so in the setState function we can simply merge the new state with the old without having to add additional code to avoid mutating the state when updating.

function setState(state, newState) {

return state.merge(newState);

}

We will also need a method for updating the filters based on the user’s interactions, so let’s add that to the reducer as well:

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

switch (action.type) {

...

case 'CHANGE_FILTER':

return changeFilter(state, action.filter);

...

The CHANGE_FILTER action will occur when a checkbox is clicked, so our changeFilter implementation should 1. Identify which member of the filter list initiated the CHANGE_FILTER action and 2. Toggle the value of that member’s inuse property and 3. Return the updated filter values as part of the new state:

function getFilterIndex(state, itemId) {

return state.get('filters').findIndex(

(item) => item.get('id') === itemId

);

}

function changeFilter(state, filter) {

let filterIndex = getFilterIndex(state,filter)

const updatedFilter = state.get('filters')

.get(filterIndex)

.update('inuse', inuse => inuse === false ? true : false);

return state.update('filters', filters => filters.set(filterIndex, updatedFilter));

}

With the SET_STATE and CHANGE_FILTER actions described in the reducer, we can now create the store used to dispatch our initial SET_STATE command to in index.js:

import reducer from './reducer'

const store = createStore(reducer)

set_state(get_campgrounds(features))

Where get_campgrounds is the function that converts the geoJSON features to a list of objects for the campgrounds state element referenced in the set_state function.

We can now render our app, passing the Redux store as the Provider for the app. This enables the app to interact with the Redux dispatcher to process state changes:

ReactDOM.render(

<Provider store={store}>

<CampFilterAppContainer />

</Provider>,

document.getElementById('root')

);

With the Redux dispatcher set up, we can now move on to creating the app container, where we will map the actions for our app back to the dispatcher and set the props to be used by the app.

The action_creators.js file is our link between the delegate props that will be sent to the children components and the Redux store. In action_creators.js we define a function to handle filter changes:

export function changeFilter(filter) {

return {

type: 'CHANGE_FILTER',

filter

}

}

The changeFilter function returns an action with parameters type and filter, which might look familiar to you looking back at reducer.js:

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

switch (action.type) {

...

case 'CHANGE_FILTER':

return changeFilter(state, action.filter);

...

In addition to the action creators, we need to map the state from the Redux provider to the props that will be passed to the React components. In addition, we will perform the filtering of the campgrounds in the app container before sending the campgrounds down to the components to get rendered. First, let’s get the active filters, those with inuse true, so we know how to filter the campgrounds. From CampFilterApp.jsx:

function mapStateToProps(state) {

let filters = state.get('filters')

let campgrounds = state.get('campgrounds')

let filtered_campgrounds = campgrounds

let active_filters = filters.filter(

item => item.get('inuse') === true

)

...

}

Notice that we start by setting filtered_campgrounds to the entire campgrounds list. In the event that none of the filters are selected, such as when we first load the app, we want to pass all the campgrounds to the React components. Next, we iterate through all the active filters, keeping only the campgrounds that have true properties matching the active_filters:

active_filters.forEach(filter => {

filtered_campgrounds = filtered_campgrounds.filter(

item => item.get('properties').get(filter.get('id')) === true

)

})

If there are no active filters the filtered_campgrounds list remains unchanged, meaning all campgrounds will be displayed.

Finally, now that we have the list of filtered campgrounds, we return this and the other state information as props:

return {

filters: filters,

campgrounds: filtered_campgrounds

};

Phew! We are now ready to create the app container, connecting the action creators and the mapped states to the CampFilterApp component:

export const CampFilterAppContainer = connect(mapStateToProps,actionCreators)(CampFilterApp);

Recall that CampFilterAppContainer was referenced in the ReactDOM.render() function in index.js. It’s a little circuitous, so let’s just recap what we went over here:

  1. We created the Redux store based on the methods in reducer.js, which define how changes in state are handled
  2. We created an app container to
    1. stick together the action creators, which define the interface between the components and the reducer, with
    2. the redux store state, which is mapped to properties, resulting in creating the props that will be consumed by the React components.
  3. We render this app container when the app is initialized in index.js

Last but not least, we can now render the app. From CampFilterApp.jsx:

render() {

return (

<div className="container">

<Jumbotron>

<h1>Crater Lake Camping</h1>

</Jumbotron>

<br></br>

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

</div>

)};

The props that are being passed to the React components will include the filters and filtered_campgrounds defined in index.js (mapped to props in CampFilterApp.jsx) and the changeFilter action defined in action_creators.js.  Recall from our React components above that changeFilter is attached to the filter checkbox onClick event, so our app will now filter the campgrounds.

Give it a try using the demo app on Heroku or download the code and run it for yourself. If you want to add a new filter simply add a new field to the geoJSON features and a corresponding item to the filter state list and let React/Redux take care of the rest!

Explore React Courses