Use React with Hooks to build a Chrome extension that displays the weather by calling the OpenWeaterMap API.
The most popular web browsers, Chrome and Firefox, support extensions. Extensions are small apps built using HTML, CSS, and JavaScript that you can add to your browser to get additional functionality that is not included by default. This makes extending your browser very easy. All a user needs to do is to add browser add-ons from the online stores, like the Chrome Web Store or the Firefox Store.
Browser extensions are just normal HTML apps packaged in a specific way. This means that we can use HTML, CSS, and JavaScript to build our own extensions — this also means we can use frontend JavaScript frameworks like React or Vue to build extensions.
Chrome and Firefox extensions follow the Web Extension API standard.
In this article, we will build a Chrome extension that displays the weather from the OpenWeatherMap API. We will add search to let users look up the current weather and forecast from the API and display it in the extension’s popup box.
The OpenWeatherMap API is available at https://openweathermap.org/api and you will need to register for an API key.
Getting Started
We will use React to build the browser extension. If we create the app with Create React App, we can just build the app as we normally would and then modify the manifest.json
in the project to create a valid Chrome extension.
The manifest.json
is the additional configuration file needed to tell Chrome how to handle our app as an extension. We will build the app first, and then I will show you what’s needed in manifest.json
to make it work.
To start building the app, we run Create React App by running:
npx create-react-app weather-app
This command will create the React project folder with skeleton code that we will build on.
Next we install some packages we need to build our extension. We need Axios for making HTTP requests, Bootstrap for styling, Formik for form value handling, MobX for state management, and Yup for form validation.
We install all of them by running:
npm i axios bootstrap formik mobx mobx-react react-bootstrap yup
Building the App
Now we can build our weather widget Chrome extension. In App.js
, replace the existing code with:
import React from "react";
import HomePage from "./HomePage";
import "./App.css";
import { KeywordStore } from "./store";
import Navbar from "react-bootstrap/Navbar";
const keywordStore = new KeywordStore();
function App() {
return (
<div className="App">
<Navbar bg="primary" expand="lg" variant="dark">
<Navbar.Brand href="#home">Weather App</Navbar.Brand>
</Navbar>
<HomePage keywordStore={keywordStore} />
</div>
);
}
export default App;
We add the React Boostrap Navbar
and our HomePage
in the file.
Next in App.css
, replace the existing code with:
html,
body {
min-width: 500px;
}
.page {
padding: 20px;
}
We specify the minimum width of our widget when displayed in the browser and add some padding to our 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 later in the article) 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 to 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";
const schema = yup.object({
keyword: yup.string().required("Keyword is required")
});
function HomePage({ keywordStore }) {
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);
};
React.useEffect(() => {
if (!initialized) {
keywordStore.setKeyword(localStorage.getItem("keyword") || "");
setInitialized(true);
}
});
return (
<div className="page">
<h1>Weather</h1>
<Formik
validationSchema={schema}
onSubmit={handleSubmit}
initialValues={{ keyword: localStorage.getItem("keyword") || "" }}
>
{({
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={{ marginRight: "10px" }}>
Search
</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>
);
}
export default observer(HomePage);
We have the form that lets the user enter the city of their choice into a search box. When the form is submitted, the evt.keyword
value will be set in the store. When the keyword
value in the store updates, the search functions in the CurrentWeather
and Forecast
components will be executed. We also set the keyword
in the Local Storage. Using Local Storage also allows us to persist a default value for the user based on the location that they set.
Form validation is done by the Yup library by creating an object for the form validation schema. We make the keyword
field a required field.
In the useEffect
callback, we load the keyword
from Local Storage and set it in the MobX store to trigger the searches.
At the bottom of the component, we have the tabs for displaying the current weather and forecast.
Next create a file called request.js
in the src
folder and add:
const APIURL = "http://api.openweathermap.org";
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}`
);
This the code for the HTTP requests for searching the current weather and the forecasts.
process.env.REACT_API_APIKEY
is the OpenWeatherMap API key stored in the .env
file of the project’s root folder.
It is stored as:
REACT_APP_APIKEY='OpenWeatherMap API key'
Next create a 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 };
This is the MobX store where we store the keyword
so that they can be set and accessed easily by our components. We set it in HomePage
and get it in CurrentWeather
and Forecast
components.
Packaging the App for the Browser
To create the Chrome extension, we need to modify manifest.json
.
In there, replace the existing code with:
{
"short_name": "Weather Extension",
"name": "Weather Extension",
"icons": {
"16": "favicon.ico",
"48": "logo192.png",
"128": "logo512.png"
},
"content_security_policy": "script-src 'self' 'sha256-WDhufSqZOEoWULzS4Nwz11MNyHzZClVYbQ2JSt1vfkw'; object-src 'self'",
"permissions": [],
"manifest_version": 2,
"version": "0.0.1",
"browser_action": {
"default_popup": "index.html",
"default_title": "Weather Widget"
}
}
Some of these key-values must be set the way they are in this file. manifest_version
must be set to 2
, and version
must be greater than 0.
Also, we need:
"browser_action": {
"default_popup": "index.html",
"default_title": "Weather Widget"
}
This shows the pop-up when clicking on the extension’s icon after we install the extension.
Loading the App in Chrome
Next we run npm run build
to build the production files. Then go to chrome://extensions/
in Chrome. Toggle on Developer mode and click “Load unpacked”, then select the build
folder once the open file dialog comes up.