Categories
React

How to Build a Chrome Extension with React

Spread the love

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 Cards 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.

By John Au-Yeung

Web developer specializing in React, Vue, and front end development.

Leave a Reply

Your email address will not be published. Required fields are marked *