January 16th, 2018 Update
As certain sections of this guide are now obsolete, here’s an updated solution proposed by avid reader Goungaf Saâd.
You can take a look at this version in this GitHub repo. Note that he is using create-react-app on this project, so the overall set-up might be slightly different.
Goungaf is a passionate individual in love with JavaScript and cutting edge technology. He works as a freelance web and mobile developer specializing in the MEAN/MERN stacks and Ionic/React Native mobile development.
This time around, we are going to build our lovely local weather app using React, Webpack and Babel. These 3 tools in conjunction give as enormous power and awesome syntactic sugar for our code.
We’ll make use of ECMAScript 6 classes, promises and arrow functions among others, but first, we need to get a proper environment set up. Navigate to your project folder and run the following command:
npm init
Note: If you don’t have Node and NPM installed, take a quick look at this post, that will get you going.
You’ll be asked for a few things, such as app name, author, description etc. Feel free to take your time and fill them in properly, or anxiously press enter to skip everything. Once done, you’ll have a package.json file in your project directory.
Now, let’s create our folder structure:
weather-app-react | |-- dist |-- src |-- package.json
Once that’s out of the way, we are going to configure Webpack and Babel. Webpack will take care of bundling our code and dependencies (you’ll see how later on) and Babel will compile our JSX and ES6 code into JavaScript that -almost- any web browser can understand. This may seem tedious, I know, but once you go through this process a few times, it becomes second nature!
First, we’ll install webpack:
npm install --save-dev webpack
It’s a development dependency, so we use the –save-dev flag. Now, we’ll install Babel, along with the babel loader and ES6 and React presents:
npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-preset-react
We will now create a new file in the project folder and call it webpack.config.js. We are going to be working in the src directory, but we want the final files to be packaged and ready in the dist folder, so we’ll set webpack to do exactly that:
module.exports = { entry: [ "./src/entry.js" ], output: { path: __dirname + '/dist/', filename: 'bundle.js' } };
This basically means that webpack will start off by reading the entry.js file in the src directory and place the output file (named bundle.js) in the dist directory.
Now, we need to tell webpack where the modules available to our app are located and what file extensions it should attempt to use:
module.exports = { entry: [ "./src/entry.js" ], output: { path: __driname + '/dist/', filename: 'bundle.js' }, resolve: { modules: ['node_modules', 'src'], extensions: ['', '.js', '.jsx'] } };
Now, we must tell webpack to load these files through the babel loader (it will take care of compiling our code):
module.exports = { entry: [ "./src/entry.js" ], output: { path: __dirname + "/dist/", filename: "bundle.js" }, resolve: { modules: ["node_modules", "src"], extensions: [".js", ".jsx"] }, module: { loaders: [{ loader: "babel-loader", exclude: /node_modules/, test: /.jsx?$/, query: { presets: ["react", "es2015"] } }] } };
That’s all for webpack in this project. It may seem alien to someone who has not dealt with it before, but you’ll get used to it after a few projects!
Instead of bringing in more build tools and overcomplicating our environment, we’ll keep it simple this time around, the src is only going to be used for JavaScript files, so we can go ahead and place our index.html and styles.css files, along with any images or assets that we want to include straight into the dist directory.
I’m going to be using the same stylesheet as the AngularJS project uses:
@import url(https://fonts.googleapis.com/css?family=Montserrat:700|Source+Sans+Pro:200|Work+Sans:100,200); * { margin: 0; padding: 0; box-sizing: border-box; } html { font-family: 'Work Sans', Helvetica, Arial, sans-serif; color: #fff; text-align: center; } body { background-repeat: no-repeat; background-position: center center; background-attachment: fixed; -webkit-background-size: cover; -moz-background-size: cover; -o-background-size: cover; background-size: cover; } #weather-app { width: 100vw; height: 100vh; } .thunderstorm { background-image: url(img/thunderstorm.jpg); } .drizzle { background-image: url(img/drizzle.jpg); } .rain { background-image: url(img/rain.jpg); } .snow { background-image: url(img/snow.jpg); } .atmosphere { background-image: url(img/atmosphere.jpg); } .clear { background-image: url(img/clear.jpg); } .clouds { background-image: url(img/clouds.jpg); } .extreme { background-image: url(img/extreme.jpg); } .additional { background-image: url(img/other.jpg); } .unknown { background-image: url(img/other.jpg); } .main-wrapper { display: flex; justify-content: center; position: absolute; top: 0; right: 0; bottom: 0; left: 0; background-color: rgba(10, 10, 10, 0.2); } .forecast-box { align-self: center; width: 100%; max-width: 400px; height: 90vh; } .city-name { font-family: Montserrat, Helvetica, Arial, sans-serif; font-weight: 700; } .country { font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif; font-weight: 200; } .temperature { margin-top: 15px; font-weight: 100; letter-spacing: -0.3rem; font-size: 5rem; } .super-small { cursor: pointer; font-size: 1.5rem; font-weight: 200; letter-spacing: 0; }
The index.html file is going to be much simpler this time though, we just need to link up the stylesheet and bundle.js (which does not exist yet). I’m also going to create a single
element in the body, and give it an ID:
Local Weather App | React
http://bundle.js <!– It does not exists yet! –> </body> </html>
As a final step before we get to the code, let’s download react and react-dom:
npm install --save react-react-dom
Notice how we are using save and not save-dev this time.
Building the component
We can finally get started writing some JS(X) now. Create a file inside the src directory and call it WeatherApp.jsx. This file will contain (and export) our application component using class syntax. If you’ve ever used React before, you may know that you can create a new component using this syntax:
var myComponent = React.createClass({ render: function() { return (<p>Hello World!</p>); } });
We can also use ES6 classes using the following syntax:
class MyComponent extends React.Component { render() { return (<p>Hello World</p>); } }
We’ll be using class syntax (this is the way React is headed, so we may as well join in!). Thanks to webpack, we can easily import modules into our files. Our component will need to use the React library, this is how we bring it in:
import React from 'react';
We now have the React object available in this file. Webpack is smart (and we told it to look into the node_modules folder) so it knows where to look.
We can now go ahead and create our WeatherApp component:
import React from 'react'; class WeatherApp extends React.Component { render() {
Weather app will go here!
} }
That’s the base for our component. Before we make it more complicated, let’s render our our app and see it working.
Create a new file in the src directory and name it entry.js. This file will simply take our component and render it to the dom. To do this, we use the render method provided by react-dom. We are going to deal with a React component, so we’ll also bring in React:
import React from 'react'; import { render } from 'react-dom';
You may have noticed that I’ve put a pair of curly braces around render. This is because react-dom exports several items for us to import, and render is not the default.
On the other hand, React is the default class exported by the react library. This how to export default and non-default classes:
export default class MyClass {} export class MyOtherClass {}
Pretty self-explanatory, right? Let’s not get side-tracked.
We need to also bring in our WeatherApp component into the entry.js file, let’s export it as default (we aren’t going to create any other components in this file, so we’ll make it default) and then import it into entry.js:
import React from 'react'; export default class WeatherApp extends React.Component { render() { return (
Weather app will go here!
); } }
import React from 'react'; import { render } from 'react-dom'; import WeatherApp from 'WeatherApp'; // We could also use './WeatherApp' or './WeatherApp.jsx' here
We haven’t specified a route or extension for the WeatherApp file, since we set that up in webpack.config.js, but we could always do so.
Now that we have all of the pieces, let’s finish up the puzzle and render our component to the page. Remember that empty div we created in the index.html file? Remember that we gave it an ID? Let’s use it:
import React from 'react'; import { render } from 'react-dom'; import WeatherApp from 'WeatherApp'; render(<WeatherApp />, document.getElementById('weather-app'));
Here, we are rendering our component into the div passed as the second argument. Now, if you open up the console in the project directory and run the following command:
webpack
A new file, named bundle.js, should appear in the dist directory. If you open up index.html in a web browser, you should see the results!
HAH! I LIED TO YOU! Your app is working, but the stylesheet is setting the text color to be white! So as long as you used the same styles as I did, you won’t see a thing!
No matter, let’s get this thing working. We are gong to start by initializing the state. If you are somewhat familiar with React, you’ve probably used the getInitialState() method to set the state. Using the class syntax, it’s a little different. Let’s set the default values for our component:
import React from 'react'; export default class WeatherApp extends React.Component { constructor() { super(); // A call to the class we are extending: React.Component this.state = { city: 'Loading...', country: 'Loading...', currentWeather: 'Loading...', currentTemperature: 0, currentUnit: 'C', availableUnit: 'F' } } render() { return (
Weather app will go here!
); } }
Now, let’s change the markup returned by render:
import React from 'react'; export default class WeatherApp extends React.Component { constructor() { super(); // A call to the class we are extending: React.Component this.state = { city: 'Loading...', country: 'Loading...', currentWeather: 'Loading...', currentTemperature: 0, currentUnit: 'C', availableUnit: 'F' } } render() { return (
{this.state.city}
{this.state.country}
{this.state.currentTemperature} °{this.state.currentUnit} / {this.state.availableUnit}
{this.state.currentWeather}
</div> ); } }
If you rebuild bundle.js (by executing the webpack command) and open up index.html, you should see something similar to this:
Getting geolocation data
The base is there. Now, we simply need to get this data from somewhere! We’ll need to fetch the weather data using the OpenWeatherMap API. They provide a few different interfaces, I’ll use coordinates myself.
To get these coordinates, we are going to use the browsers geolocation capabilities, and fall back to ipinfo when not available. Let’s create a new method inside our class, we’ll call getLocationCoords. We are going to make use of the ES6 promise object to return the results asynchronously, along with a third party library that will deal with AJAX requests (superagent).
Let’s start by bringing in superagent and superagent-jsonp. The latter will let us request data from a different origin (not our domain) via jsonp. Download the libraries by running:
npm install --save superagent superagent-jsonp
And now, import them into WeatherApp.jsx:
import React from 'react'; import superagent from 'superagent'; import jsonp from 'superagent-jsonp'; export default class WeatherApp extends React.Component { // ....
We can now go ahead and create the getLocationCoords method:
import React from 'react'; import superagent from 'superagent'; import jsonp from 'superagent-jsonp'; export default class WeatherApp extends React.Component { constructor() { super(); this.state = { city: 'Loading...', country: 'Loading...', currentWeather: 'Loading...', currentTemperature: 0, currentUnit: 'C', availableUnit: 'F' } } render() { return (
{this.state.city}
{this.state.country}
{this.state.currentTemperature} °{this.state.currentUnit} / {this.state.availableUnit}
{this.state.currentWeather}
</div> ); } getLocationCoords() { var deferred = Promise.defer(); if (window.navigator.geolocation) { window.navigator.geolocation.getCurrentPosition( (location) => { deferred.resolve({ lat: location.coords.latitude, lon: location.coords.longitude }); }, (error) => { deferred.reject(error); } ); } else { // Use ipinfo fallback } return deferred.promise; } }
For more details on how promises, arrow functions and the geolocation API works, take a look at the AngularJS tutorial for this app. Now that we are able to retrieve geolocation data from our browser, we need to design a fallback method for browsers without this capability. We’ll do so by using superagent and ipinfo.
import React from 'react'; import superagent from 'superagent'; import jsonp from 'superagent-jsonp'; export default class WeatherApp extends React.Component { constructor() { super(); this.state = { city: 'Loading...', country: 'Loading...', currentWeather: 'Loading...', currentTemperature: 0, currentUnit: 'C', availableUnit: 'F' } } render() { return (
{this.state.city}
{this.state.country}
{this.state.currentTemperature} °{this.state.currentUnit} / {this.state.availableUnit}
{this.state.currentWeather}
</div> ); } getLocationCoords() { var deferred = Promise.defer(); if (window.navigator.geolocation) { window.navigator.geolocation.getCurrentPosition( (location) => { deferred.resolve({ lat: location.coords.latitude, lon: location.coords.longitude }); }, (error) => { deferred.reject(error); } ); } else { superagent.get(‘http://ipinfo.io/json’) .use(jsonp) // We use superagent-jsonp .end((error, locationData) => { if (error) { deferred.reject(error); // Got an error, reject promise } else { deferred.resolve(locationData); // Got data, resolve promise } }); } return deferred.promise; } }
As you can see, I’ve create a function that returns a promise. This promise will be resolved as soon as we get results back from ipinfo. We now need to create a new method that will first get the location coordinates, and then use these to get the weather data from OpenWeatherMap. Our component is starting to become a bit… cluttered. Why don’t we move the getLocationCoords method somewhere else before we proceed any further?
I’m going to create a new file in the src directory and call it api.js, in this file, we’ll define a few functions and export them, so we can use them from our component. Do not forget to import both superagent and superagent-jsonp into this file!
import superagent from 'superagent'; import jsonp from 'superagent-jsonp'; export function getLocationCoords() { var deferred = Promise.defer(); if (window.navigator.geolocation) { window.navigator.geolocation.getCurrentPosition( (location) => { deferred.resolve({ lat: location.coords.latitude, lon: location.coords.longitude }); }, (error) => { deferred.reject(error); } ); } else { superagent.get('http://ipinfo.io/json') .use(jsonp) // We use superagent-jsonp .end((error, locationData) => { if (error) { deferred.reject(error); } else { deferred.resolve(locationData); } }); } return deferred.promise; }
We can now safely remove the entire getLocationCoords method from the WeatherApp component.
Let’s write up the getWeatherData method now. We’ll do so inside api.js:
import superagent from 'superagent'; import jsonp from 'superagent-jsonp'; export function getWeatherData(units, coords) { var deferred = Promise.defer(); var parsedUnits = units === 'C' ? 'metric' : 'imperial'; superagent.get('http://api.openweathermap.org/data/2.5/weather') .query({ units: parsedUnits, lat: coords.lat, lon: coords.lon, appid: 'YOUR_API_KEY' }) .use(jsonp) .end((error, weatherData) => { if (error) { deferred.reject(error); } else { deferred.resolve(weatherData); } }); return deferred.promise; } export function getLocationCoords() { // ..... }
Now, back to WeatherData.jsx, we must remove the entire getLocationCoords method, and import it (alongside getWeatherData) into the file:
import React from 'react'; import { getLocationCoords, getWeatherData } from 'api'; export default class WeatherApp extends React.Component { constructor() { super(); this.state = { city: 'Loading...', country: 'Loading...', currentWeather: 'Loading...', currentTemperature: 0, currentUnit: 'C', availableUnit: 'F' } } render() { return (
{this.state.city}
{this.state.country}
{this.state.currentTemperature} °{this.state.currentUnit} / {this.state.availableUnit}
{this.state.currentWeather}
</div> ); } // getLocationCoords is now gone! }
Now that the logic is in place, let’s do the following when our component is initialized:
- Get the geolocation data.
- Fetch the weather.
- Update the state.
We can do so by creating a new method in our component, I’ll call it fetchWeather:
import React from 'react'; import { getLocationCoords, getWeatherData } from 'api'; export default class WeatherApp extends React.Component { constructor() { super(); this.state = { city: 'Loading...', country: 'Loading...', currentWeather: 'Loading...', currentTemperature: 0, currentUnit: 'C', availableUnit: 'F' } } render() { return (
{this.state.city}
{this.state.country}
{this.state.currentTemperature} °{this.state.currentUnit} / {this.state.availableUnit}
{this.state.currentWeather}
</div> ); } fetchWeather(units) { getLocationCoords().then( (coords) => { // Got location data, get weather now getWeatherData(units, coords).then( (weatherData) => { // Got weather data, proceed to update state using setState() this.setState({ city: weatherData.body.name, country: weatherData.body.sys.country, currentTemperature: weatherData.body.main.temp, currentWeather: weatherData.body.weather[0].main, currentUnit: units, availableUnit: units === ‘C’ ? ‘F’ : ‘C’ }); }, (error) => { // Could not get weather data. console.error(error); } ); }, (error) => { // Could not get location data. console.error(error); } ); } }
Now, we’ll sexy it up by changing the background image to match that of the current weather. More details on that in the AngularJS tutorial.
I’m going to create a function that returns a class based on the weather ID given. We’ll then set the body element to have that class:
import React from 'react'; import { getLocationCoords, getWeatherData } from 'api'; export default class WeatherApp extends React.Component { constructor() { super(); this.state = { city: 'Loading...', country: 'Loading...', currentWeather: 'Loading...', currentTemperature: 0, currentUnit: 'C', availableUnit: 'F' } } render() { return (
{this.state.city}
{this.state.country}
{this.state.currentTemperature} °{this.state.currentUnit} / {this.state.availableUnit}
{this.state.currentWeather}
</div> ); } fetchWeather(units) { getLocationCoords().then( (coords) => { // Got location data, get weather now getWeatherData(units, coords).then( (weatherData) => { // Got weather data, proceed to update state using setState() this.setState({ city: weatherData.body.name, country: weatherData.body.sys.country, currentTemperature: weatherData.body.main.temp, currentWeather: weatherData.body.weather[0].main, currentUnit: units, availableUnit: units === ‘C’ ? ‘F’ : ‘C’ }); document.querySelector(‘body’).className = getWeatherClass(weatherData.body.weather[0].id); }, (error) => { // Could not get weather data. console.error(error); } ); }, (error) => { // Could not get location data. console.error(error); } ); function getWeatherClass(code) { if (code >= 200 && code < 300) { return ‘thunderstorm’; } else if (code >= 300 && code < 400) { return ‘drizzle’; } else if (code >= 500 && code < 600) { return ‘rain’; } else if (code >= 600 && code < 700) { return ‘snow’; } else if (code >= 700 && code < 800) { return ‘atmosphere’; } else if (code === 800) { return ‘clear’; } else if (code >= 801 && code < 900) { return ‘clouds’; } else if (code >= 900 && code < 907) { return ‘extreme’; } else if (code >= 907 && code < 1000) { return ‘additional’; } else { return ‘unknown’; } } } }
Now, we just need to call this method when the component is initialized or the user wishes to change the units. For that, we’ll first need to change the render method to do this:
render() { return (
{this.state.city}
{this.state.country}
{this.state.currentTemperature} °{this.state.currentUnit} / {this.state.availableUnit}
{this.state.currentWeather}
</div> ); }
Notice how we use bind in the onClick event to set this to equal the class itself in the function call, and this.state.availableUnit as the first argument. This is important when using class syntax, we’ll also need to do it for the componentDidMount method, which will fetch us the current weather as soon as the component is initialized:
import React from 'react'; import { getLocationCoords, getWeatherData } from 'api'; export default class WeatherApp extends React.Component { constructor() { super(); this.state = { city: 'Waiting for geolocation data...', country: 'Loading...', currentWeather: 'Loading...', currentTemperature: 0, currentUnit: 'C', availableUnit: 'F' }; this.componentDidMount = this.componentDidMount.bind(this); } componentDidMount() { this.fetchWeather(this.state.currentUnit); } fetchWeather(units) { getLocationCoords().then( (coords) => { getWeatherData(units, coords).then( (weatherData) => { this.setState({ city: weatherData.body.name, country: weatherData.body.sys.country, currentTemperature: weatherData.body.main.temp, currentWeather: weatherData.body.weather[0].main, currentUnit: units, availableUnit: units === 'C' ? 'F' : 'C' }); document.querySelector('body').className = getWeatherClass(weatherData.body.weather[0].id); }, (error) => { // Could not get weather data. console.error(error); }); }, (error) => { // Could not get location data. console.error(error); } ); function getWeatherClass(code) { if (code >= 200 && code < 300) { return 'thunderstorm'; } else if (code >= 300 && code < 400) { return 'drizzle'; } else if (code >= 500 && code < 600) { return 'rain'; } else if (code >= 600 && code < 700) { return 'snow'; } else if (code >= 700 && code < 800) { return 'atmosphere'; } else if (code === 800) { return 'clear'; } else if (code >= 801 && code < 900) { return 'clouds'; } else if (code >= 900 && code < 907) { return 'extreme'; } else if (code >= 907 && code < 1000) { return 'additional'; } else { return 'unknown'; } } } render() { return (
{this.state.city}
{this.state.country}
{this.state.currentTemperature} °{this.state.currentUnit} / {this.state.availableUnit}
{this.state.currentWeather}
</div> ); } }
As you can see, we first bind componentDidMount to use the current class as this and then simply call fetchWeather in it. The weather app is not complete! Run webpack in your console again and server your index.html! (Note that geolocation may not work if you don’t use a proper http server and just open up index.html in a browser)
This app could still use improvement, but you can get a broad idea of what React and webpack can achieve together, it may seem overkill for a simple app such as this one, but will prove invaluable on larger projects!