DEV Community

Cover image for Build a Chrome Extension Using ReactJS
an-object-is-a
an-object-is-a

Posted on • Updated on • Originally published at anobjectisa.com

Build a Chrome Extension Using ReactJS

This tutorial assumes you know how a Chrome Extension works.

If you need to know how they work, check out my video tutorial here:

Or my dev.to article here:

. . .

Let’s Begin.

We’ll start by initializing npm.
>>npm init -y

Then we’ll install all of the packages we’ll need.

First the dev dependencies.
>>npm install --save-dev @babel/core @babel/plugin-proposal-class-properties @babel/preset-env @babel/preset-react babel-loader copy-webpack-plugin clean-webpack-plugin html-loader html-webpack-plugin webpack webpack-cli webpack-dev-server

Then the non-dev dependencies.
>>npm install react react-dom react-router-dom

In the ‘package.json’ we’ll write scripts for our development and production.

Under “scripts”, we’ll add,

// package.json

"build": "webpack-dev-server",
"build:prod": "webpack -p"
Enter fullscreen mode Exit fullscreen mode

. . .

Let’s create the ReactJS files.

Create a ‘src’ folder for these files.
Create a ‘components’ folder inside the ‘src’ folder for the ReactJS components we’ll be writing.

It’s important that we cover all of our Google Chrome Extension bases.
This means we’ll need a “foreground or content” page, “popup” page, and “options” page minimum.

The other files, “background script”, “manifest”, and “icons” will come later.

The architecture of our ReactJS files is this:

  1. An entry point — this is an HTML file with a “div” we can inject into
  2. An initial render file — this is a JavaScript file that injects one ReactJS component into the entry point
  3. An initial ReactJS component file — this is a JavaScript file that we’ll use as the HTML to initially render

 
Let’s create the entry points for the “foreground”, “popup”, and “options”.
The code is all the same except for the “id” we give the “div”.

// foreground.html

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
</head>
<body>
    <div id="foreground"></div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode
// popup.html

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
</head>
<body>
    <div id="popup"></div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode
// options.html

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
</head>
<body>
    <div id="options"></div>    
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

 
Let’s create the initial render file for the “foreground”, “popup”, and “options”.
The code is all the same except for the name of the import.

// index-foreground.js

import React from 'react';
import { render } from 'react-dom';
import Foreground from './components/Foreground.js';
render(<Foreground />, document.querySelector('#foreground'));
Enter fullscreen mode Exit fullscreen mode
// index-popup.js

import React from 'react';
import { render } from 'react-dom';
import Popup from './components/Popup.js';
render(<Popup />, document.querySelector('#popup'));
Enter fullscreen mode Exit fullscreen mode
// index-options.js

import React from 'react';
import { render } from 'react-dom';
import Options from './components/Options.js';
render(<Options />, document.querySelector('#options'));
Enter fullscreen mode Exit fullscreen mode

 
Let’s create the initial ReactJS component for the “foreground”, “popup”, and “options”.

Here, you’re free to create your ReactJS app.

// components/Foreground.js

import React from 'react';
function Foreground() {
    return (
        <div style={styles.main}>
            <h1>Chrome Ext - Foreground</h1>
        </div>
    )
}
const styles = {
    main: {
        position: 'absolute',
        top: '50%',
        left: '50%',
        transform: 'translate(-50%, -50%)',
        zIndex: '1000',
        fontSize: '80px',
        pointerEvents: 'none'
    }
}
export default Foreground;
Enter fullscreen mode Exit fullscreen mode
// components/Popup.js

import React from 'react';
function Popup() {
    return (
        <div style={styles.main}>
            <h1>Chrome Ext - Popup</h1>
        </div>
    )
}
const styles = {
    main: {
        width: '300px',
        height: '600px'
    }
}
export default Popup;
Enter fullscreen mode Exit fullscreen mode
// components/Options.js

import React from 'react';
import {
    BrowserRouter as Router,
    Switch,
    Route,
    Link,
    Redirect
} from "react-router-dom";
import Popup from './Popup.js';
import Foreground from './Foreground.js';
function Options() {
    return (
        <Router>
            <div style={styles.container}>
                <div style={styles.nav_bar}>
                    <h1>Chrome Ext - Options</h1>
                    <nav>
                        <ul>
                            <li>
                                <Link to="/">Options</Link>
                            </li>
                            <li>
                                <Link to="/popup">Popup</Link>
                            </li>
                            <li>
                                <Link to="/foreground">Foreground</Link>
                            </li>
                        </ul>
                    </nav>
                </div>
                <Switch>
                    <Route exact path="/popup">
                        <Popup />
                    </Route>
                    <Route exact path="/foreground">
                        <Foreground />
                    </Route>
                    <Route exact path="/">
                        <Redirect to="/options.html" />
                    </Route>
                </Switch>
            </div>
        </Router>
    )
}
const styles = {
    container: {
        display: 'flex',
        flexDirection: 'column',
        justifyContent: 'center',
        alignItems: 'center'
    }
}
export default Options;
Enter fullscreen mode Exit fullscreen mode

Note:
Notice how the CSS of this project is in the ReactJS files. We won’t be doing
separate CSS files for this tutorial.

. . .

Let’s create the Chrome Extension-specific files.

We’ll need:

  • a ‘manifest.json’ file
  • a ‘background.js’ file
  • a script file to inject our ‘foreground.html’ “div” (this is VERY important)
  • icons x 4 Our ‘manifest.json’ doesn’t need anything special. Just a normal manifest with background, options, and popup pages specified.
// manifest.json

{
    "name": "ReactJS Chrome Extension",
    "description": "Using ReactJS to build a Chrome Extension",
    "version": "0.1.0",
    "manifest_version": 2,
    "icons": {
        "16": "./obj-16x16.png",
        "32": "./obj-32x32.png",
        "48": "./obj-48x48.png",
        "128": "./obj-128x128.png"
    },
    "background": {
        "scripts": ["./background.js"]
    },
    "options_page": "./options.html",
    "browser_action": {
        "default_popup": "popup.html"
    },
    "permissions": [
        "tabs",
        "<all_urls>"
    ] 
}
Enter fullscreen mode Exit fullscreen mode


 

Our ‘background.js’ is unique in its work-flow.

Here’s the problem we need to solve:
When we’re developing our ReactJS app, the ‘index-foreground.js’ JSX file injects into the ‘foreground.html’s ‘div’.

When we move to a Chrome Extension, we want to inject a ‘foreground’ or ‘content’ script into the page the user is viewing.

Problem is, they don’t have the architecture for this.
There is no ‘foreground.html’ ‘div’.

We need to create this element on their page BEFORE we inject our
index-foreground.js’ into their page.

// background.js

chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
    if (changeInfo.status === 'complete' && 
        tab.url.includes('http')) {
        chrome.tabs.executeScript(tabId, { file: 
            './inject_script.js' }, function () {
            chrome.tabs.executeScript(tabId, { file: 
               './foreground.bundle.js' }, function () {
                   console.log('INJECTED AND EXECUTED');
            });
        });
    }
});
Enter fullscreen mode Exit fullscreen mode
// inject-script.js

const foreground_entry_point = document.createElement('div');
let reactJS_script = document.createElement('script');

foreground_entry_point.id = 'foreground';

foreground_entry_point.appendChild(reactJS_script);
document.querySelector("body").appendChild(foreground_entry_point);
Enter fullscreen mode Exit fullscreen mode

Note:
I’ve created an ‘inject-script.js’ file that creates that special ‘foreground.html’ ‘div’.

 
 
The icons are self-explanatory.

. . .

THE MOST IMPORTANT STEP IN THIS TUTORIAL.

DO NOT SKIP THIS INFORMATION.

THIS IS HOW WE GET A REACTJS PROJECT TO “CONVERT” INTO A CHROME EXTENSION.

 

Let’s deal with the ‘webpack.config.js’.

I’m not going to go into too much detail about how exactly ‘webpack’ works.
I will, however, explain what the file is saying.

What are we doing with ‘webpack’?

We have a lot of files.
Some of those files are ‘html’.
Some are ‘pngs’ or ‘jpgs’.
Some are JavaScript exclusively.
Some are a mix of JavaScript and JSX (ReactJS).

We need to get ‘webpack’ to transpile some of these items and simply copy the others.

Specifically, we want to transpile the JSX into Javascript (this is what those babel dependencies are for) and we want to copy our html, icon, and JavaScript-only files.

Note:
So what’s happening here?

  • HtmlWebpackPlugin’ allows us to copy html files into a target destination.
  • CopyWebpackPlugin’ allows us to copy any file, without transformations, into a target destination.
  • CleanWebpackPlugin’ is used to make sure all files in a target destination are deleted before building.
  • devServer’ is needed for developing ReactJS in real-time.
  • entry’ defines three points. These are the JSX initial render files that inject our JSX into the html entry-point files.
  • output’ tells ‘webpack’ to name them as ‘bundles’ and where to save them.
  • module’ is where we tell ‘webpack’ how to deal with different types of files. For all JavaScript/JSX files included in the ‘webpack’ chain, transpile the code. For all HTML files, transform them so they’re ready for ‘webpack’ to merge them with our ‘bundles’.
  • plugins’ run after individual files get transformed with modules. We are merging our HTML files with the appropriate ‘bundles’(chunks) and exporting them (either to server or hard-drive), copying files we don’t want transpiled, and running our cleaner.

. . .

We have our developer environment setup and the files all filled out.

How do we run this thing?
Two ways.

For development, run the ‘build’ command.
>>npm run build

View your ReactJS app in the browser.
localhost:8080

For production, we need to tweak our ‘webpack.config.js’ then run the ‘build:prod’ command.
 

comment out the ‘HtmlWebpackPlugin’ for the foreground
We don’t need to export the ‘foreground.html’ file into production because we’re creating our own ‘foreground.html’ ‘div’ using the ‘background.js’ and ‘inject-script.js’ scripts.

 
Now run the production script.
>>npm run build:prod

Load your production extension in your Chrome Browser.

Choose the ‘dist’ directory.
 
 

Keep in mind, this is an experimental and a bit “hacky” solution to using ReactJS to create Google Chrome Extensions.

I’ve tried to make the development and production pipeline as streamlined as is possible.
I haven’t done extensive testing on a lot of ‘npm’ packages.
From what I have tried, ‘react-router-dom’ works.
You’re mileage may vary.

. . .

You can get the source files here.

If you would like a more in-depth guide, check out my full video tutorial on YouTube, An Object Is A.

Build a Chrome Extension with React (2020 Web Development)

Top comments (1)

Collapse
 
knoguchi profile image
Kenji Noguchi • Edited

Very nice. I've setup React+TypeScript following this doc.