A simple weather app with React

Nitin Sampathi
Mar 12, 2018 · 15 min read
Image for post
Image for post

The Goal

Planning

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

Setup

heroku-cra-node

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
Image for post
Image for post
Everything in weather-test/

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

The Front-end

componentDidMount()

componentDidMount() { let cachedLat = localStorage.getItem(‘latitude’);
let cachedLon = localStorage.getItem(‘longitude’);
cachedLat ?
this.setCoordsFromLocalStorage(cachedLat, cachedLon) :
this.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,
});
});
}
}
localStorage.setItem(‘latitude’, position.coords.latitude);
localStorage.setItem(‘longitude’, position.coords.longitude);
this.callWeatherApi(position.coords.latitude, position.coords.longitude, “geo”).

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;
}
};
let response = await fetch(‘/api/weather?latitude=’ + latitude + ‘&longitude=’ + longitude + ‘&location=’ + location);
‘/api/weather?latitude=’ + latitude + ‘&longitude=’ + longitude + ‘&location=’ + location
if (body.cod == 404) {
throw Error(body.message);
}
this.callUnsplashApi(body.name)
this.setState({
errorText: “”,
data: body,
loading: false
})

setCoordsFromLocalStorage()

// componentDidMount()
cachedLat ?
this.setCoordsFromLocalStorage(cachedLat, cachedLon) :
this.getCoords();
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));
});
}

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)
);
});
}
this.callWeatherApi(“latitude”, “longitude”, this.state.location)
.then(res => this.setState({ response: res.express }))
.catch(err =>
this.setState({
...
}),

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;
};
currentCityImage: body[randomPhotoNumber].urls.regular,
userFirstName: body[randomPhotoNumber].user.first_name,
userProfileLink: body[randomPhotoNumber].user.links.html,
userProfileImage: body[randomPhotoNumber].user.profile_image.medium

A recap so far

The Back-end

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

dotenv

touch .env
defaults write com.apple.finderAppleShowAllFiles -boolean true ; killall Finder
UNSPLASH_ID=YOUR_UNSPLASH_API_KEY
require(‘dotenv’).config()
unsplash.init(process.env.UNSPLASH_ID);

Heroku Config Variables

Image for post
Image for post
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”;

/api/weather

let response = await fetch(‘/api/weather?latitude=’ + latitude + ‘&longitude=’ + longitude + ‘&location=’ + location);
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);
});
}
});
// 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;
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);
});
}

/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);
});
});
// App.js, callUnsplashApi()
fetch(‘/api/unsplash?location=’ + location);
// server/index.js, app.get(‘/api/unsplash’)
let location = req.query.location;
unsplash.searchPhotos(location, null, null, null, function(error,
photos, link) {
body = photos;
res.send(body);
});

proxy

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

render()

<Searchbar>

// react-ui/src/App.js
<Searchbar errorClass={this.state.errorClass}
onSubmit={this.changeLocation}
onClick={this.changeLocation}/>
// 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>
// react-ui/src/Searchbar.js
handleChange(evt) {
evt.preventDefault();
const location = this.textInput.value;
this.props.onClick(location);
this.textInput.value = ‘’;
}

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}
/>
}

Deployment

Create a production build of your app

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

Push to heroku

heroku create
git push heroku master

Automatic deployment

Make it a chrome extension

Final Thoughts

Pixels in Progress

Designing and building digital products.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store