Many apps want to get data based on location. This is where the HTML Geolocation API comes in. You can use it easily to get the location of the current device via the Internet.
To get the location of the device the browser is running with plain JavaScript, we write:
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(getPosition);
}
function getPosition(position) {
console.log(position.coords.latitude, position.coords.longitude);
}
As you can see, getting the latitude and longitude is very easy. We can also easily add geolocation to any React app. The react-geolocated package is a great add-on for adding geolocation capabilities to your app. It is located at https://www.npmjs.com/package/react-geolocated.
It provides a promise-based API for getting the location of the device, so we can easily use async
and await
to get the location with this package.
In this article, we will build an app to weather app that lets you get the current weather and forecast of the current location you’re in. It also lets you search the weather by inputting the location manually.
To start we will run Create React App to create the React project. Run:
npx create-react-app weather-app
to create the project.
Next, we have to add some libraries. We need Axios for making HTTP requests, Formik and Yup for form value handling and validation, MobX for state management, Bootstrap for styling, React Geolocated for getting geolocation data, and React Router for routing. We install them by running:
npm i axios formik mobx mobx-react react-bootstrap react-geolocated react-router-dom yup
With all the packages installed, we can start building the app. We start by replacing the code in App.css
:
.page {
padding: 20px;
}
to add some padding to our page.
Next, replace the code in App.js
with:
import React from "react";
import { Router, Route } from "react-router-dom";
import HomePage from "./HomePage";
import { createBrowserHistory as createHistory } from "history";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import "./App.css";
const history = createHistory();
function App({ keywordStore }) {
return (
<div className="App">
<Router history={history}>
<Navbar bg="primary" expand="lg" variant="dark">
<Navbar.Brand href="#home">Geolocation Weather App</Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav">
<Nav className="mr-auto">
<Nav.Link href="/" active>
Home
</Nav.Link>
</Nav>
</Navbar.Collapse>
</Navbar>
<Route
path="/"
exact
component={props => (
<HomePage {...props} keywordStore={keywordStore} />
)}
/>
</Router>
</div>
);
}
export default App;
Here we add the React Bootstrap navigation bar and the route to our home page.
Next, we create a component to display the current weather. Create a file called CurrentWeather.js
in the src
folder and add:
import React from "react";
import { observer } from "mobx-react";
import { searchWeather } from "./request";
import ListGroup from "react-bootstrap/ListGroup";
function CurrentWeather({ keywordStore }) {
const [weather, setWeather] = React.useState({});
const getWeatherForecast = async keyword => {
const response = await searchWeather(keyword);
setWeather(response.data);
};
React.useEffect(() => {
keywordStore.keyword && getWeatherForecast(keywordStore.keyword);
}, [keywordStore.keyword]);
return (
<div>
{weather.main ? (
<ListGroup>
<ListGroup.Item>
Current Temparature: {weather.main.temp - 273.15} C
</ListGroup.Item>
<ListGroup.Item>
High: {weather.main.temp_max - 273.15} C
</ListGroup.Item>
<ListGroup.Item>
Low: {weather.main.temp_min - 273.15} C
</ListGroup.Item>
<ListGroup.Item>Pressure: {weather.main.pressure} </ListGroup.Item>
<ListGroup.Item>Humidity: {weather.main.humidity}</ListGroup.Item>
</ListGroup>
) : null}
</div>
);
}
export default observer(CurrentWeather);
We get the search keyword from the keywordStore
which we will build and search for the current weather when keywordStore.keyword
is updated by passing it in the array of the second argument of the React.useEffect
function.
Once the data is retrieved, we set the data with the setWeather
function and display the data in a ListGroup
at the bottom of the page.
We wrap observer
around our component in the last line so that we get the latest keyword
value from keywordStore
.
Next, we add a component for display the forecast, add Forecast.js
in the src
folder and add:
import React from "react";
import { observer } from "mobx-react";
import { searchForecast } from "./request";
import ListGroup from "react-bootstrap/ListGroup";
import Card from "react-bootstrap/Card";
function Forecast({ keywordStore }) {
const [forecast, setForecast] = React.useState({});
const getWeatherForecast = async keyword => {
const response = await searchForecast(keyword);
setForecast(response.data);
};
React.useEffect(() => {
keywordStore.keyword && getWeatherForecast(keywordStore.keyword);
}, [keywordStore.keyword]);
return (
<div>
{Array.isArray(forecast.list) ? (
<div>
{forecast.list.map(l => {
return (
<Card body>
<ListGroup>
<ListGroup.Item>Date: {l.dt_txt}</ListGroup.Item>
<ListGroup.Item>
Temperature: {l.main.temp - 273.15} C
</ListGroup.Item>
<ListGroup.Item>
High: {l.main.temp_max - 273.15} C
</ListGroup.Item>
<ListGroup.Item>
Low: {l.main.temp_min - 273.15} C
</ListGroup.Item>
<ListGroup.Item>Pressure: {l.main.pressure} C</ListGroup.Item>
</ListGroup>
</Card>
);
})}
</div>
) : null}
</div>
);
}
export default observer(Forecast);
It’s very similar to CurrentWeather.js
except that the data is in a list. So we map forecast.list
into an array of Card
s instead of just rendering it.
Next, we create a HomePage.js
file in the src
folder and add:
import React from "react";
import { Formik } from "formik";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import { observer } from "mobx-react";
import * as yup from "yup";
import Tabs from "react-bootstrap/Tabs";
import Tab from "react-bootstrap/Tab";
import CurrentWeather from "./CurrentWeather";
import Forecast from "./Forecast";
import { geolocated } from "react-geolocated";
import { getLocationByLatLng } from "./request";
const schema = yup.object({
keyword: yup.string().required("Keyword is required")
});
const buttonStyle = { marginRight: "10px" };
function HomePage({ keywordStore, coords }) {
const [initialized, setInitialized] = React.useState(false);
const handleSubmit = async evt => {
const isValid = await schema.validate(evt);
if (!isValid) {
return;
}
localStorage.setItem("keyword", evt.keyword);
keywordStore.setKeyword(evt.keyword);
};
const getWeatherCurrentLocation = async () => {
const { latitude, longitude } = coords;
const { data } = await getLocationByLatLng(latitude, longitude);
const keyword = (
data.results[0].address_components.find(c =>
c.types.includes("locality")
) || {}
).long_name;
localStorage.setItem("keyword", keyword);
keywordStore.setKeyword(keyword);
};
const clear = () => {
localStorage.clear();
keywordStore.setKeyword("");
};
React.useEffect(() => {
if (!initialized) {
keywordStore.setKeyword(localStorage.getItem("keyword") || "");
setInitialized(true);
}
}, [keywordStore.keyword]);
return (
<div className="page">
<h1>Weather</h1>
<Formik
validationSchema={schema}
onSubmit={handleSubmit}
initialValues={{ keyword: localStorage.getItem("keyword") || "" }}
enableReinitialize={true}
>
{({
handleSubmit,
handleChange,
handleBlur,
values,
touched,
isInvalid,
errors
}) => (
<Form noValidate onSubmit={handleSubmit}>
<Form.Row>
<Form.Group as={Col} md="12" controlId="keyword">
<Form.Label>City</Form.Label>
<Form.Control
type="text"
name="keyword"
placeholder="City"
value={values.keyword || ""}
onChange={handleChange}
isInvalid={touched.keyword && errors.keyword}
/>
<Form.Control.Feedback type="invalid">
{errors.keyword}
</Form.Control.Feedback>
</Form.Group>
</Form.Row>
<Button type="submit" style={buttonStyle}>
Search
</Button>
<Button
type="button"
onClick={getWeatherCurrentLocation}
style={buttonStyle}
disabled={!coords}
>
Get Weather of Current Location
</Button>
<Button type="button" onClick={clear}>
Clear
</Button>
</Form>
)}
</Formik>
<br />
<Tabs defaultActiveKey="weather">
<Tab eventKey="weather" title="Current Weather">
<CurrentWeather keywordStore={keywordStore} />
</Tab>
<Tab eventKey="forecast" title="Forecast">
<Forecast keywordStore={keywordStore} />
</Tab>
</Tabs>
</div>
);
}
HomePage = observer(HomePage);
export default geolocated({
positionOptions: {
enableHighAccuracy: false
},
userDecisionTimeout: 5000
})(HomePage);
We use the React-Geolocated package here to add the geolocation functionality to our app. The package provides us with the geolocated
higher-order component to let users get their current location if they allow our app to do so. We set enableHighAccuracy
to false
so that our app will respond faster by using a less accurate location, which is already accurate in most cases. userDecisionTimeout
is set so that after 5 seconds, geolocation will be assumed to be disabled by the user.
Since we wrapped our HomePage
component with the geolocated
component, we will get the coords
prop so that we get the location when geolocation is enabled. We add check for the coords
prop in our ‘Get Weather of Current Location‘ button so that users can only use the geolocation functionality when they have geolocation enabled.
In the callback for the useEffect
hook, we set the array in the second argument to watch for keywordStore.keyword
changes so that it will re-render when that changes. We need this so that we get the latest keyword
into our form via the intialValues
prop of our Formik
component. When we click the ‘Get Weather of Current Location‘ button, we get set the keyword in both local storage and the MobX store, so we can just watch the store to make it render the local storage’s keyword value.
In the getWeatherCurrentLocation
function, we get the location with the Google Maps API, then get the city name by finding the component with locality
in the types
property, and get the long_name
from that, then set the keyword
in our MobX store for use by the previous components we created and also in local storage so that we see it in our form and keep the same value set after refreshing the value.
Next in index.js
replace the existing code with:
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { KeywordStore } from "./store";
const keywordStore = new KeywordStore();
ReactDOM.render(
<App keywordStore={keywordStore} />,
document.getElementById("root")
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: [https://bit.ly/CRA-PWA](https://bit.ly/CRA-PWA)
serviceWorker.unregister();
to pass or MobX store into our app.
Then in we create requests.js
in the src
folder, and add the following:
const APIURL = "http://api.openweathermap.org";
const GOOGLE_MAP_API_URL = "https://maps.googleapis.com/maps/api/geocode/json";
const axios = require("axios");
export const searchWeather = loc =>
axios.get(
`${APIURL}/data/2.5/weather?q=${loc}&appid=${process.env.REACT_APP_APIKEY}`
);
export const searchForecast = loc =>
axios.get(
`${APIURL}/data/2.5/forecast?q=${loc}&appid=${process.env.REACT_APP_APIKEY}`
);
export const getLocationByLatLng = (lat, lng) =>
axios.get(
`${GOOGLE_MAP_API_URL}?latlng=${lat},${lng}&key=${process.env.REACT_APP_GOOGLE_APIKEY}`
);
We have functions to make HTTP requests to get the weather and location with the Google API. To get the properties in process.env
, put them in your .env
file in your project’s root folder with those field properties as keys and the corresponding API keys as the values.
After that, create store.js
in the src
folder and add:
import { observable, action, decorate } from "mobx";
class KeywordStore {
keyword = "";
setKeyword(keyword) {
this.keyword = keyword;
}
}
KeywordStore = decorate(KeywordStore, {
keyword: observable,
setKeyword: action
});
export { KeywordStore };
to create our MobX Store. We have the keyword
field that is watched by other components by wrappingobserver
function outside our component when we export them. Also, we have the setKeyword
function to set the latest values of the keyword
field.
Finally, in index.js
, replace the existing code with:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Geolocation Weather App</title>
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
crossorigin="anonymous"
/>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
to change the title and add Bootstrap CSS.
After all the hard work, we run npm start
to start our app. Then we get: