A simple weather app with React

In this post we’ll explain how to build a weather app, like this one we built. Matt and I decided to create this as our hello world to developing something and we think it’ll be helpful to explain this process, our process. It may not be the best way or the right way, but it’s a way, so we’re hoping it’ll help someone else who’s just getting started with React.

At the time we started this project, we had just finished the ReactJS Codecademy course and a good bit of Javascript Essential Training on Lynda. We suggest checking out the repo beforehand and trying to understand what’s going on in there on your own. We’ll cover the main aspects of the app and skip over some smaller details that you can try to figure out on your own. By the end of this article you’ll be able to see how this weather app is built so that you can modify it for your specific project needs.

The Goal

A weather app is a pretty typical project which people have taken in a bunch of directions. But, we’ll keep it pretty straight forward. We just want to grab some data from an API and display it. And show a pretty picture!

Planning

It helps defining what you want the app to do. We want to grab the user’s coordinate location, get the weather, and then grab an image — to use for the background. It would also be cool to let the user search for the weather. So, we’ll grab the value of the input field, which would be a location, then get the weather and then grab an image — to use for the background.

Describing the intentions of your app, step by step, helps you think about how you might build it. Seems like we’ll get a location, then grab the weather, and then an image, at least twice.

Once we figure out what the app does, we can settle on what we’ll need. We’re gonna use React for the front-end, node/express for the back-end, and use the OpenWeatherMap and Unsplash APIs.

And, because React is made up of components, it helps mapping them out so you can begin to think about state and how data might pass down through props.

The final app, but it might as well be a mockup (left), mapping out the components to begin with (right)

Setup

heroku-cra-node

This is a repo that’s comprised of just create-react-app with a Node server for Heroku. It’s got everything we need to build and deploy our app.

git clone https://github.com/mars/heroku-cra-node.git //clone repo
mv heroku-cra-node weather-test //rename the directory
cd weather-test //go into the directory
Everything in weather-test/

Everything in react-ui/ is from create-react-app. server/ has our server file, index.js, who’s package.json is in the root directory. All we need to worry about for now is react-ui/src/App.js.

Follow the Local Development section on the heroku-cra-node documentation page to run both the front and back-end servers at the same time and get the project up and running locally.

Dependencies

We’ll need to npm install a few dependencies before we get started. Request to make API requests for the weather data, Unsplash to grab the images, and dotenv to hide our API keys during development.

In your terminal, in the root directory:

npm i — save request unsplash-api dotenv

You should see index.js’ package.json (which is in the root) update to reflect these dependencies.

// weather-test/package.json
“dependencies”: {
“dotenv”: “^5.0.1”,
“express”: “^4.14.1”,
“request”: “^2.83.0”,
“unsplash-api”: “^1.2.0”
},

API keys

Grab an API key from OpenWeatherMap and Unsplash. We’ll deal with these when we work on the back-end.

The Front-end

Now we’ll get started on creating the interface of the application which all takes place in react-ui/src/App.js.

componentDidMount()

React has a few special methods which get called throughout the component lifecycle, which is the lifespan of the component. If you need something to happen at a certain time, a lifecycle method may be the best time to call that function.

One of these lifecycle methods is componentDidMount() and it gets called only once when the component first loads (mounts).

componentDidMount() {
 let cachedLat = localStorage.getItem(‘latitude’);
let cachedLon = localStorage.getItem(‘longitude’);
 cachedLat ? 
this.setCoordsFromLocalStorage(cachedLat, cachedLon) :
this.getCoords();
}

At this time, well set two variables, cachedLat and cachedLon. We’ll set them to their corresponding value in the localStorage object. We can set and get items from the localStorage object when we want to remember information across sessions. When our user first visits our app, cachedLat won’t exist. But, after we get the user’s coordinates we’ll set those items in localStorage for next time.

So, every time our component mounts we’ll check to see if cachedLat exists with a Ternary Operator, which is essentially just a simplified if/else statement. If it exists, we’ll use the coordinates we already have set in the localStorage object (cachedLat and cachedLon) and call setCoordsFromLocalStorage(). And if doesn’t exist, which will only be the case on the first time a user visits the app, we’ll have to get the coordinates with getCoords().

getCoords()

getCoords() {
if (window.navigator.geolocation) {
navigator.geolocation.getCurrentPosition((position) => {
localStorage.setItem(‘latitude’, position.coords.latitude);
localStorage.setItem(‘longitude’, position.coords.longitude);
this.callWeatherApi(position.coords.latitude,
position.coords.longitude,
“geo”)
.then(res => this.setState({ response: res.express }))
.catch(err => console.log(err));
}, (error) => {
this.setState({
error: error.message,
});
});
}
}

In order to get the user’s coordinates we’ll use the Geolocation API, specifically the getCurrentPosition() method.

We’ll then use a callback function which takes position as its sole parameter.

localStorage.setItem(‘latitude’, position.coords.latitude);
localStorage.setItem(‘longitude’, position.coords.longitude);

Once we have latitude and longitude, we’ll set these items in the localStorage object, so that we can reference them the next time the app loads.

this.callWeatherApi(position.coords.latitude, position.coords.longitude, “geo”).

Then, we pass those coordinates to callWeatherApi() which has already been defined above. Note that we’re passing 3 arguments to callWeatherApi(), the user’s latitude, longitude, and a string with a value of “geo”.

Since we’re getting the weather data with callWeatherApi three times, once with coordinates when the app first loads, with coordinates saved in the localStorage object, and then through the search search bar for whatever the user searches for, we want to have one function that handles all that logic. When we’re using coordinates, we’ll pass along “geo” as the third argument. If we’re not using coordinates, so whenever the user searches for something, we won’t pass “geo” but instead pass the user’s search term. Later, when we make the actual weather data API request, we’ll check to see what that third argument is. If it’s “geo” we know we’re getting the weather data with coordinates. If it’s not “geo”, the user must have entered something in the search bar.

callWeatherApi()

callWeatherApi = async (latitude, longitude, location) => {
 let response = await fetch(‘/api/weather?latitude=’ + latitude +
‘&longitude=’ + longitude + ‘&location=’ + location);
let body = await response.json();
 if (body.cod == 404) {
throw Error(body.message);
} else {
this.callUnsplashApi(body.name)
this.setState({
errorText: “”,
data: body,
loading: false
})
return body;
}
};

callWeatherAPI() is defined right above changeLocation(), and it references those three arguments that were passed to it as latitude, longitude, and location.

let response = await fetch(‘/api/weather?latitude=’ + latitude + ‘&longitude=’ + longitude + ‘&location=’ + location);

Here we’re sending those arguments to our server file in /server/index.js which is where we actually get the weather data. In our server file, there is a GET request with a matching path of /api/weather. Any variables we append to this path here in App.js, we can use when we make our API call in /server/index.js.

‘/api/weather?latitude=’ + latitude + ‘&longitude=’ + longitude + ‘&location=’ + location

We separate the variables section of this path with a question mark and separate each variable with an ampersand. When we get the weather data, we’ll send it back to App.js as an object called body.

if (body.cod == 404) {
throw Error(body.message);
}

Then we’ll check to see if body comes back with a 404 and throw an Error if it does.

this.callUnsplashApi(body.name)

If it doesn’t, and all is good, we’ll find an image from that city. Using the name property on the body object, we use body.name to access the city’s name. We pass that name to the callUnsplashApi() function and then setState().

this.setState({
errorText: “”,
data: body,
loading: false
})

When we setState(), we’ll set any errorText to an empty string, the data in state to the body object containing all the weather data, and loading to false.

Since state is now set, all of our components that rely on something in state will re-render. But before we talk about render(), there are a couple other ways we callWeatherApi() which we need to cover.

setCoordsFromLocalStorage()

If you noticed in getCoords(), once we getCurrentPosition() from window.navigator.geolocation, we set the latitude and longitude in the localStorage object.

// componentDidMount()
cachedLat ?
this.setCoordsFromLocalStorage(cachedLat, cachedLon) :
this.getCoords();

So, in componentDidMount(), every subsequent time the user visits our app, cachedLat will exist, since we’ve ran getCoords() and set latitude and longitude in localStorage. Meaning, we won’t have to getCoords(), we can just setCoordsFromLocalStorage() with cachedLat and cachedLon.

setCoordsFromLocalStorage(cachedLat, cachedLon) {
this.setState({
latitude: cachedLat,
longitude: cachedLon
}, () => {
this.callWeatherApi(this.state.latitude,
this.state.longitude,
“geo”)
.then(res => this.setState({ response: res.express }))
.catch(err => console.log(err));
});
}

setCoordsFromLocalStorage() takes cachedLat and cachedLon, sets them in state, and then runs callWeatherApi() the same way we called it in getCoords(), with “geo” as a string for the third argument.

changeLocation()

changeLocation(location) {
this.setState({
location: location
}, () => {
this.callWeatherApi(“latitude”, “longitude”, this.state.location)
.then(res => this.setState({ response: res.express }))
.catch(err =>
this.setState({
errorText: “city does not exist”,
}),
console.log(this.state.errorText)
);
});
}

changeLocation() is a method that takes one argument and is the other way we callWeatherApi(). We pass a location to this method from the search bar and then set it in state. We pass callWeatherApi() as a callback to setState().

this.callWeatherApi(“latitude”, “longitude”, this.state.location)
.then(res => this.setState({ response: res.express }))
.catch(err =>
this.setState({
...
}),

This is similar to what we’ve seen before except instead of passing “geo” as the third argument, we’re passing a location — the location that the user entered into the search bar.

We don’t need to worry exactly how changeLocation() is being fired just yet, so for now just know that when a user types a location into the search bar and clicks submit, this function is fired.

So, back in callWeatherApi(), when we called it in changeLocation(), everything is still the same. We append three arguments to a url path and send it off to our server with the fetch command. If the weather data for that location comes back, we callUnsplashApi() with the city’s name to get the image and then set state with that image data.

callUnsplashAPI

callUnsplashApi = async (location) => {
let response = await fetch(‘/api/unsplash?location=’ + location);
let body = await response.json();

if (response.status !== 200) throw Error(body.message);
 var randomPhotoNumber = Math.floor(Math.random() * 10);
this.setState({
currentCityImage: body[randomPhotoNumber].urls.regular,
userFirstName: body[randomPhotoNumber].user.first_name,
userProfileLink: body[randomPhotoNumber].user.links.html,
userProfileImage:
body[randomPhotoNumber].user.profile_image.medium
});
return body;
};

One last thing we need to cover is callUnsplashApi() which gets called every time we callWeatherApi() and is passed the name of the city which we’re getting the weather data from.

This works similarly to callWeatherAPI(). We append only one argument, a location, to a path matching a GET request in server/index.js. In this case its /api/unsplash and then we send it off to our server with the fetch command. If the image data for that location comes back, we setState() with the data.

currentCityImage: body[randomPhotoNumber].urls.regular,
userFirstName: body[randomPhotoNumber].user.first_name,
userProfileLink: body[randomPhotoNumber].user.links.html,
userProfileImage: body[randomPhotoNumber].user.profile_image.medium

Specifically, we have components that need the image itself, the user’s name who took the image, the link to their profile page, and their profile image.

A recap so far

So whether we have coordinates from the browser or a user’s search term, we need the weather for that location. So, we callWeatherApi() and set that data into state. We’re sending three arguments every time. When we’re retrieving weather data with coordinates, which happens the every time the app first loads, either by getting them from window.navigator.geolocation or localStorage, we’re sending one argument, the third argument, as a string — “geo”. We also retrieve weather data through the user’s search term in the search bar, but in this case when we callWeatherApi() we’re sending the third argument as the user’s search term, not “geo”. Later, in our server file, server/index.js, we’ll check what the third argument is, if it’s “geo” or not, so we can request the weather data with coordinates or with the name of the city that the user searched for. And, whenever we get the weather data for a city, we need the image of that city, so we callUnsplashAPI() and also set that data into state.

Now we’re ready to actually obtain that data from the API calls which all takes place in server/index.js.

The Back-end

Now we need to actually get the weather data when our front-end requests it and send it back to the front-end. First we need to require the dependencies we previously installed. At the top of index.js in server/ add:

const request = require(‘request’);
const unsplash = require(‘unsplash-api’);
require(‘dotenv’).config()

With the unsplash-api, we’ll have to initialize it with

unsplash.init(YOUR_UNSPLASH_API_KEY);

When you signup for API access with Unsplash or OpenWeatherMap, you’ll get a special key that allows you to use their data acquiring service. You can use your keys directly, but you’ll likely want to keep these secret. When your project is in a development environment, meaning, you’re working on your app locally and it’s on github, you can hide your keys with a module called dotenv—which we’ve already downloaded. When your project is on production, meaning deployed to heroku, you can enter your API keys into the project settings page on heroku, and the keys that your API was referencing in development will work just the same. Here’s how to do both:

dotenv

This is a module that helps us hide our API keys. In your terminal, in the root directory, make a file called .env.

touch .env

.env is a hidden file because it starts with .. To see hidden files in your finder, enter this command in your terminal:

defaults write com.apple.finderAppleShowAllFiles -boolean true ; killall Finder

Open up the file in atom and add in your API keys on new lines in the form of NAME=VALUE

UNSPLASH_ID=YOUR_UNSPLASH_API_KEY

Then require the module in server/index.js (which we’ve already done above)

require(‘dotenv’).config()

Now those variables you’ve declared in the .env file is available to you through the process.env object. For example, instead of initializing Unsplash with the API key directly, we can reference the variable in our .env file.

unsplash.init(process.env.UNSPLASH_ID);

Now by referencing process.env.YOUR_API_KEYS, you can access the variables stored in .env.

Heroku Config Variables

When you deploy, you won’t be pushing up your .env file, which is what your server is looking for locally. But you can supplement the variables in the .env file by using heroku’s config variables. They will act as variables in a .env file so there is no need to change the way they’re referenced in your server file. You can still use the process.env and it will reference whatever keys and values you put into heroku.

The only other thing we do in server/index.js is declare some variables so we can reference them later when we make the API calls.

var weatherKey = process.env.WEATHER_KEY //Nitin’s API key
var locationURLPrefix =
http://api.openweathermap.org/data/2.5/weather?q=";
var coordsURLPrefix =
http://api.openweathermap.org/data/2.5/weather?";
var urlSuffix = ‘&APPID=’ + weatherKey + “&units=imperial”;

Now that we have our API keys ready to go, all of the necessary variables created to make the API calls, we can begin making requests to get the weather and image data.

/api/weather

let response = await fetch(‘/api/weather?latitude=’ + latitude + ‘&longitude=’ + longitude + ‘&location=’ + location);

In App.js, inside of callWeatherAPI(), we’re using fetch and passing a path. The first portion of the path, before the question mark, needs to match the path of the route in server/index.js. In this case it’s /api/weather. We’re appending variables to this path so that we can reference them in the back-end which will help us make the right API call.

app.get(‘/api/weather’, (req, res) => {
let location = req.query.location;
let latAndLon = “lat=” + req.query.latitude + ‘&’ + “lon=” +
req.query.longitude;
 if (location == “geo”) {
let url = coordsURLPrefix + latAndLon + urlSuffix;
request(url, function(error, response, body) {
res.send(body);
});
} else {
let url = locationURLPrefix + location + urlSuffix;
request(url, function(error, response, body) {
res.send(body);
});
}
});

When we call fetch, that path needs to match a GET request we make in our server file. In server/index.js, we have a GET request with the/api/weather path.

// In App.js we use this path:
// fetch('/api/weather?latitude=' + latitude + '&longitude=' +
longitude + '&location=' + location);
// In server/index.js, app.get('/api/weather')
let location = req.query.location;
let latAndLon = “lat=” + req.query.latitude + ‘&’ + “lon=” +
req.query.longitude;

We can grab those variables that we appended to the path in App.js through the req.query object.

if (location == “geo”) {
let url = coordsURLPrefix + latAndLon + urlSuffix;
request(url, function(error, response, body) {
res.send(body);
});
} else {
let url = locationURLPrefix + location + urlSuffix;
request(url, function(error, response, body) {
res.send(body);
});
}

In order to figure out how we’re requesting weather data, we’ll check to see if location is equal to the string “geo”. If it is, we know that we have real coordinates, so we’ll construct a URL variable equal to a url that includes the latitude and longitude data and the coordinate URL prefix, coordsURLPrefix. This custom URL is sent using the request module which helps us make HTTP requests and then we send body back through the res object. The same applies for when location does not equal “geo”, except we create a URL with the location URL prefix, locationURLPrefix , and the location data, not latitude and longitude.

/api/unsplash

app.get(‘/api/unsplash’, (req, res) => {
let location = req.query.location;
unsplash.searchPhotos(location, null, null, null, function(error,
photos, link) {
body = photos;
res.send(body);
});
});

The other request we want to make is to Unsplash, to get the image data. This follows a very similar process.

// App.js, callUnsplashApi()
fetch(‘/api/unsplash?location=’ + location);

In App.js, we use fetch with a path that must match a GET request in server/index.js. Also in App.js, we append data to that path, specifically the user’s entry into the search bar as a variable called location.

// server/index.js, app.get(‘/api/unsplash’)
let location = req.query.location;

In server/index.js, when the path, /api/unsplash is matched, we set a location variable from the path through the req.query object.

unsplash.searchPhotos(location, null, null, null, function(error,
photos, link) {
body = photos;
res.send(body);
});

Then we use the unsplash-api module and the method that comes with it, searchPhotos, passing in the location as our search term. We set body equal to the photos object that is returned from Unsplash and then send body back to App.js.

proxy

One last, very important, thing to do is connect the front-end to the back-end. They do have matching paths but App.js doesn’t know where this server lives. In react-ui/package.json add “proxy”: “http://localhost:8000".

In server/index.js we’re listening to port 8000.

const PORT = process.env.PORT || 8000;
app.listen(PORT, function () {
console.error(`Node cluster worker ${process.pid}: listening on
port ${PORT}`);
});

render()

The final thing we need to cover is back in the front-end, in react-ui/app.js, and it’s what happens after when we setState(). When we setState() with the weather and image data, we re-render our components.

<Searchbar>

// react-ui/src/App.js
<Searchbar errorClass={this.state.errorClass}
onSubmit={this.changeLocation}
onClick={this.changeLocation}/>

The Searchbar component gets passed three props. onSubmit and onClick are referencing App.js’ changeLocation() method.

// react-ui/src/Searchbar.js
<form onSubmit={this.handleChange}>
<input className={this.props.errorClass} ref={(input) => {
this.textInput = input; }} type=”text” />
<button onClick={this.handleChange}> Submit </button>
</form>

In react-ui/src/Searchbar.js we’re rendering a form with a text input and buttom. The form and button have event listeners which listen to the onSubmit and onClick events, respectively.

// react-ui/src/Searchbar.js
handleChange(evt) {
evt.preventDefault();
const location = this.textInput.value;
this.props.onClick(location);
this.textInput.value = ‘’;
}

When they fire, they call Searchbar’s handleChange() function which passes the textInput.value back to App.js and fires changeLocation() with the search term as location.

this.state.loading

{
this.state.loading ?
<div className=”loading”><p>loading…</p></div> :
<Info
errorText={this.state.errorText}
formError={this.state.formError}
location={this.state.location}
lat={this.state.latitude}
lon={this.state.longitude}
city={this.state.data.name}
temp={this.state.data.main.temp}
humidity={this.state.data.main.humidity}
weather=
{this.state.data.weather[Object.keys(this.state.data.weather)
[0]].description}
windSpeed={this.state.data.wind.speed}
/>
}

Here we’re using another ternary operator to check if this.state.loading is true. If it is, which it will be when the app first loads, we’ll display a div that renders text that says “loading…”. We set loading to false when we set the weather data into state. When loading is false, we’ll stop rendering the div and render the Info component. The Info component simply displays our weather data. It also passes some data to its child component which displays Sentence which is a description of the current weather conditions.

Unsplashuser and Unsplash

Unsplashuser is the user’s profile image and name, linked out to their Unsplash profile page. Unsplash is just a div which acts as a background image for the app.

Deployment

Create a production build of your app

Optimize performance by creating a production build of your app.

npm run build

The server is set up to use a directory called build

// server/index.js
app.use(express.static(path.resolve(__dirname, ‘../react-ui/build’)));

Push to heroku

Download, install, and log into heroku and then run:

heroku create
git push heroku master

Automatic deployment

If your project is on github, you can set up automatic deployment to update your heroku remote every time you push to github. Find it in the Deploy tab of your project settings

Make it a chrome extension

We had originally wanted to make this a chrome extension, but decided not to when we hit a few snags in the project and needed to focus on the functionality.

Final Thoughts

A weather app is a great first project. It requires you to create an interface, obtain some data, and then display it. If you want to add a bit more complexity, then create a way for the user to interact with it. These basic functions and requirements can be a foundation to much more complex and intricate projects. Creating a weather app is a great way to get started on that project path.