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.
Fork
andclone
this repository.cd
into the project directory.- Run
npm i
to install dependencies. - Run
npm start
to 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
promiseMiddleware
fromredux-promise-middleware
. - Import
applyMiddleware
fromredux
. - Modify the original
createStore
to 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
axios
at the top of the file. - Create a new action type of
SET_WEATHER
that 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
URL
that equals the return value frombuildURL
.buildURL
gets imported fromweatherUtils.js
. It takes alocation
parameter and returns an API url we can use with axios.
- This function should create a variable called
promise
that equals a promise usingaxios.get
and theURL
variable we just created.- The
then
of the promise should capture the response and then return the value offormatWeatherData( response.data )
. formatWeatherData
gets imported fromweatherUtils.js
. It takes the object the API returns and formats it for our application to use.
- The
- This function should
return
an 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
reducer
to handle theSET_WEATHER
action:- When the action type is
SET_WEATHER + "_PENDING"
:-
Object
return { error: false, loading: true, search: false, weather: {} };
-
- When the action type is
SET_WEATHER + "_FULFILLED"
:-
Object
return { error: false, loading: false, search: false, weather: action.payload };
-
- When the action type is
SET_WEATHER + "_REJECTED"
:-
Object
return { 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
src
namedapiKey.js
. - In
src/apiKey.js
export 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_KEY
fromsrc/apiKey.js
. - Modify the
BASE_URL
variable 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
setWeather
fromsrc/ducks/weather.js
. - Add
setWeather
to the object in theconnect
statement. - Modify the
handleSubmit
method:- 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 thereturn
statement 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.error
is truthy, we will render theErrorMessage
component with a reset prop equal to ourreset
action creator. - If
props.loading
is truthy, we will render an image with asrc
prop equal tohourglass
.hourglass
is an animated loading indicator. - If
props.search
is truthy, we will render theEnterLocation
component. - If none of those are truthy, we will render the
CurrentWeather
component with a reset prop equal to ourreset
action creator and a weather prop equal toweather
off of props.
- Open
src/App.js
. - Create a method above the
render
method calledrenderChildren
:- This method should deconstruct
error
,loading
,search
,weather
, andreset
fromprops
for 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
weather
prop 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.