In this project we'll be building a weather app that allows users to search for the current weather anywhere in the world. We'll make use of the OpenWeatherMap API and Redux Promise Middleware to accomplish this in a user friendly fashion.
- Go to OpenWeatherMap and create an account. You'll need an API key to complete this project.
- The API key can take up to 10 minutes to activate.
Forkandclonethis repository.cdinto the project directory.- Run
npm ito install dependencies. - Run
npm startto spin up the development server.
We will begin this project by installing new dependencies and modifying the store to handle promises.
- Run
npm install redux-promise-middleware axios. - Open
src/store.js. - Import
promiseMiddlewarefromredux-promise-middleware. - Import
applyMiddlewarefromredux. - Modify the original
createStoreto have two additional parameters afterweather:undefined- This could be an initial state, but since the reducer is handling that, let's just passundefined.applyMiddleware( promiseMiddleware() )- This will tell Redux that we want the middleware called on every action that is dispatched.
src/store.js
import { createStore, applyMiddleware } from "redux";
import promiseMiddleware from "redux-promise-middleware";
import weather from "./ducks/weather";
export default createStore( weather, undefined, applyMiddleware( promiseMiddleware() ) );In this step, we will add an action for fetching weather data and handle all possible outcomes in the reducer in src/ducks/weather.js.
- Open
src/ducks/weather.js. - Import
axiosat the top of the file. - Create a new action type of
SET_WEATHERthat equals"SET_WEATHER". - Create and export a new action creator called
setWeather:- This function should take a single parameter called
location. - This function should create a variable called
URLthat equals the return value frombuildURL.buildURLgets imported fromweatherUtils.js. It takes alocationparameter and returns an API url we can use with axios.
- This function should create a variable called
promisethat equals a promise usingaxios.getand theURLvariable we just created.- The
thenof the promise should capture the response and then return the value offormatWeatherData( response.data ). formatWeatherDatagets imported fromweatherUtils.js. It takes the object the API returns and formats it for our application to use.
- The
- This function should
returnan object with two properties:type- This should equal our action type:SET_WEATHER.payload- This should equal the promise we created above:promise.
- This function should take a single parameter called
- Update the
reducerto handle theSET_WEATHERaction:- When the action type is
SET_WEATHER + "_PENDING":-
Objectreturn { error: false, loading: true, search: false, weather: {} };
-
- When the action type is
SET_WEATHER + "_FULFILLED":-
Objectreturn { error: false, loading: false, search: false, weather: action.payload };
-
- When the action type is
SET_WEATHER + "_REJECTED":-
Objectreturn { error: true, loading: false, search: false, weather: {} };
-
- When the action type is
src/ducks/weather.js
import { buildURL, formatWeatherData } from '../utils/weatherUtils';
import axios from 'axios';
const initialState = {
error: false,
loading: false,
search: true,
weather: {}
};
const RESET = "RESET";
const SET_WEATHER = "SET_WEATHER";
export default function weather( state = initialState, action ) {
switch ( action.type ) {
case SET_WEATHER + "_PENDING":
return {
error: false,
loading: true,
search: false,
weather: {}
};
case SET_WEATHER + "_FULFILLED":
return {
error: false,
loading: false,
search: false,
weather: action.payload
};
case SET_WEATHER + "_REJECTED":
return {
error: true,
loading: false,
search: false,
weather: {}
};
case RESET: return initialState;
default: return state;
}
}
export function reset() {
return { type: RESET };
}
export function setWeather( location ) {
var url = buildURL( location );
const promise = axios.get( url ).then( response => formatWeatherData( response.data ) );
return {
type: SET_WEATHER,
payload: promise
}
}In this step, we will create a file that contains and exports our API Key from OpenWeatherMap.
- Create a new file in
srcnamedapiKey.js. - In
src/apiKey.jsexport default your API Key in a string.- You can locate your API Key here after you've signed up and logged in.
src/apiKey.js
export default "API_KEY_HERE";In this step, we will update our weatherUtils file to handle constructing a URL that will be used to call the OpenWeatherMap API.
- Open
src/utils/weatherUtils.js. - Import
API_KEYfromsrc/apiKey.js. - Modify the
BASE_URLvariable to equal:`http://api.openweathermap.org/data/2.5/weather?APPID=${ API_KEY }&units=imperial&`
src/utils/weatherUtils.js
import cloudy from "../assets/cloudy.svg";
import partlyCloudy from "../assets/partly-cloudy.svg";
import rainy from "../assets/rainy.svg";
import snowy from "../assets/snowy.svg";
import sunny from "../assets/sunny.svg";
import unknownIcon from "../assets/unknown-icon.svg";
import API_KEY from "../apiKey";
const BASE_URL = `http://api.openweathermap.org/data/2.5/weather?APPID=${ API_KEY }&units=imperial&`;
function isZipCode( location ) { return !isNaN( parseInt( location ) ); }
function getWeatherIcon( conditionCode ) { if ( conditionCode === 800 ) { return sunny; } if ( conditionCode >= 200 && conditionCode < 600 ) { return rainy; } if ( conditionCode >= 600 && conditionCode < 700 ) { return snowy; } if ( conditionCode >= 801 && conditionCode <= 803 ) { return partlyCloudy; } if ( conditionCode === 804 ) { return cloudy; } return unknownIcon; }
export function formatWeatherData( weatherData ) { return { icon: getWeatherIcon( weatherData.weather[ 0 ].id ), currentTemperature: weatherData.main.temp, location: weatherData.name, maxTemperature: weatherData.main.temp_max, minTemperature: weatherData.main.temp_min, humidity: weatherData.main.humidity, wind: weatherData.wind.speed }; }
export function buildURL( location ) { if ( isZipCode( location ) ) { return BASE_URL + `zip=${location}`; } return BASE_URL + `q=${location}`; }In this step, we will fetch the weather data from OpenWeatherMap's API and place it on application state.
- Open
src/components/EnterLocation/EnterLocation.js. - Import
setWeatherfromsrc/ducks/weather.js. - Add
setWeatherto the object in theconnectstatement. - Modify the
handleSubmitmethod:- This method should call
setWeather( remember it is on props ) and pass inthis.state.location.
- This method should call
- Open
src/ducks/weather.js. - Add a
console.log( action.payload )before thereturnstatement in theSET_WEATHER + '_FULFILLED'case.
Try entering in a zip code or location in the interface and press submit. You should now see a console.log appear in the debugger console.
src/components/EnterLocation/EnterLocation.js
import React, { Component } from "react";
import { connect } from "react-redux";
import { setWeather } from '../../ducks/weather';
import "./EnterLocation.css";
class EnterLocation extends Component {
constructor( props ) {
super( props );
this.state = { location: "" };
this.handleChange = this.handleChange.bind( this );
this.handleSubmit = this.handleSubmit.bind( this );
}
handleChange( event ) {
this.setState( { location: event.target.value } );
}
handleSubmit( event ) {
event.preventDefault();
this.props.setWeather( this.state.location )
this.setState( { location: "" } );
}
render() {
return (
<form
className="enter-location"
onSubmit={ this.handleSubmit }
>
<input
className="enter-location__input"
onChange={ this.handleChange }
placeholder="London / 84601"
type="text"
value={ this.state.location }
/>
<button
className="enter-location__submit"
>
Submit
</button>
</form>
);
}
}
export default connect( state => state, { setWeather })( EnterLocation );In this step, we will be displaying all the different child components based on application state.
- If
props.erroris truthy, we will render theErrorMessagecomponent with a reset prop equal to ourresetaction creator. - If
props.loadingis truthy, we will render an image with asrcprop equal tohourglass.hourglassis an animated loading indicator. - If
props.searchis truthy, we will render theEnterLocationcomponent. - If none of those are truthy, we will render the
CurrentWeathercomponent with a reset prop equal to ourresetaction creator and a weather prop equal toweatheroff of props.
- Open
src/App.js. - Create a method above the
rendermethod calledrenderChildren:- This method should deconstruct
error,loading,search,weather, andresetfrompropsfor simplified referencing. - This method should selectively render a component based on the conditions specified in the summary.
- This method should deconstruct
- Replace
<EnterLocation />in the render method with the invocation ofrenderChildren.
src/App.js
import React, { Component } from "react";
import { connect } from "react-redux";
import "./App.css";
import hourglass from "./assets/hourglass.svg";
import { reset } from "./ducks/weather";
import CurrentWeather from "./components/CurrentWeather/CurrentWeather";
import EnterLocation from "./components/EnterLocation/EnterLocation";
import ErrorMessage from "./components/ErrorMessage/ErrorMessage";
class App extends Component {
renderChildren() {
const {
error,
loading,
search,
weather,
reset
} = this.props;
if ( error ) {
return <ErrorMessage reset={ reset } />
}
if ( loading ) {
return (
<img alt="loading indicator" src={ hourglass } />
)
}
if ( search ) {
return <EnterLocation />
}
return (
<CurrentWeather reset={ reset } weather={ weather } />
)
}
render() {
return (
<div className="app">
<h1 className="app__title">WEATHERMAN</h1>
{ this.renderChildren() }
</div>
);
}
}
export default connect( state => state, { reset } )( App );In this step, we will update CurrentWeather to display an icon and the actual weather information.
- Open
src/components/CurrentWeather/CurrentWeather.js. - Using the
weatherprop object, replace the static data for location, icon, current temp, max temp, min temp, wind, and humidity.
src/components/CurrentWeather/CurrentWeather.js
import React, { PropTypes } from "react";
import "./CurrentWeather.css";
export default function CurrentWeather( { weather, reset } ) {
const {
currentTemperature,
humidity,
icon,
location,
maxTemperature,
minTemperature,
wind
} = weather;
return (
<div className="current-weather">
<div className="current-weather__weather">
<h3 className="current-weather__location"> { location } </h3>
<img
alt="current weather icon"
className="current-weather__icon"
src={ icon }
/>
<h3 className="current-weather__temp"> { currentTemperature }° </h3>
<div className="current-weather__separator" />
<ul className="current-weather__stats">
<li className="current-weather__stat">Max: { maxTemperature }°</li>
<li className="current-weather__stat">Min: { minTemperature }°</li>
<li className="current-weather__stat">Wind: { wind } MPH</li>
<li className="current-weather__stat">Humidity: { humidity }%</li>
</ul>
</div>
<button
className="current-weather__search-again"
onClick={ reset }
>
Search Again
</button>
</div>
);
}
CurrentWeather.propTypes = {
reset: PropTypes.func.isRequired
, weather: PropTypes.shape( {
icon: PropTypes.string.isRequired
, currentTemperature: PropTypes.number.isRequired
, maxTemperature: PropTypes.number.isRequired
, minTemperature: PropTypes.number.isRequired
, wind: PropTypes.number.isRequired
, humidity: PropTypes.number.isRequired
} ).isRequired
};If you see a problem or a typo, please fork, make the necessary changes, and create a pull request so we can review your changes and merge them into the master repo and branch.
© DevMountain LLC, 2017. Unauthorized use and/or duplication of this material without express and written permission from DevMountain, LLC is strictly prohibited. Excerpts and links may be used, provided that full and clear credit is given to DevMountain with appropriate and specific direction to the original content.
