Build modern chrome extension with React.js

Clarence Chen
FLUX Tech Blog
Published in
6 min readSep 15, 2021
picture referred from https://keepcodeclean.com/how-to-create-a-chrome-extension/

You might have experience using a chrome extension that could add extra and customized functionalities to your browser. To develop an extension, the most normal way is to follow the official guide. To put it simply, it’s based on native HTML/CSS/Javascript to provide extension UI and communication between the browser background page and the page you are viewing.

However, in some use cases, as we do in FLUX Inc, we require it to be able to perform multiple operations and user experiences, the way we do in a website. Native HTML/CSS/Javascript is not easy to develop. Therefore, we come up with the idea to develop a chrome extension with React.js, a powerful framework well-known for building interactive websites.

Since FLUX is a digital marketing company working on banner ads on web pages, we are interested in building extensions to check ad slot configuration as simple as possible. In this article, we will show how to build a simple extension with React.js. that shows all google ads on the page you are viewing.

Note: This article assumes you have read the basics of the official guide and React.js

Preparations of files and boilerplates

Since our extension is literally a React.js app, I recommend using create-react-app CLI to set up the project.

Make sure you installed npm

npx create-react-app chrome-extension-demo

Now you have a skeleton for our react app.

cd chrome-extension-demo/public 
ls

You will see the following files:

. ├── favicon.ico 
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt

Create a few javascript files.

touch background.js content-script.js inline-script.js

Modify manifest.json as follows

{
"background": {
"persistent": true,
"scripts": [ "background.js" ]
},
"browser_action": {
"default_icon": "logo192.png",
"default_popup": "index.html",
"default_title": "Chrome extension demo"
},
"icons": {
"128": "logo512.png",
"48": "logo192.png"
},
"manifest_version": 2,
"name": "Chrome extension demo",
"permissions": [ "activeTab", "webRequest", "tabs", "\u003Call_urls>", "webRequestBlocking", "browsingData" ],
"version": "0.1",
"content_scripts": [
{
"matches": ["*://*/*"],
"js": ["content-script.js"],
"run_at": "document_end"
}
],
"web_accessible_resources": ["inline-script.js"]
}

I’ll elaborate on the details of architecture in the next section.

Collect Google ads information from page

Navigate to https://jsonformatter.curiousconcept.com/# (or any other web pages with google ads displayed).

Open javascript console Run

googletag.pubads().getSlots()[0].getAdUnitPath()

You will see something like(if google ad is working correctly):

"/124067137/curiousconcept728x90FS_1"
Sample console result

Run

googletag.pubads().getSlots()[0].getResponseInformation()

Result

"{\"advertiserId\":4574316137,\"campaignId\":2318687180,\"creativeId\":138234497649,\"lineItemId\":4688588090,\"sourceAgnosticCreativeId\":138234497649,\"sourceAgnosticLineItemId\":4688588090,\"isBackfill\":false,\"yieldGroupIds\":null,\"companyIds\":null,\"creativeTemplateId\":null,\"encryptedTroubleshootingInfo\":null}" 

Now we got all the building blocks we need for our extension. Nice work ;)

What we need to do next is to pass this information to the chrome extension and show it in the extension popup.

The architecture

For security purposes, the chrome extension is launched in a sandbox and is not allowing to access the page resource, such as Javascript Console.

There are three types of JS scripts in the chrome extension.

  • background.js

The script has control over all tabs and can communicate to all content scripts per tab.

  • content-script.js

The script has control of an individual tab and can communicate with an inline script per page.

  • inline-script.js

The script is injected directly into the page source so it can access the JS environment of the page

Therefore, to get the page content, we should let inline-script.js pass needed variables to content-script.js and pass to background.js. Then the variables of background.js can be accessed by our React app (extension popup).

The architecture of our extension

Time to get your hands dirty and write some code!

Get google ad info via inline-script.js and pass it to content-script.js

  • inline-script.js
// googletag is the global variable provided by google ad
function collectGoogletagInfo() {
if (typeof googletag === 'undefined' || typeof googletag.pubads !== 'function') {
return null;
}
const adSlots = googletag.pubads().getSlots();
const adInfo = {};
adSlots.forEach(slot => {
adInfo[slot.getAdUnitPath()] = slot.getResponseInformation();
});
return adInfo;
}
// Give it some time for google ads to load
setTimeout(() => {
var googleTaginfo = collectGoogletagInfo();
// Send data to content-script.js via javascript native custom event
document.dispatchEvent(new CustomEvent('checkResult', {
detail: JSON.stringify(googleTaginfo)
}))
}, 5000)

Pass google ad info from content-script.js to background.js

  • content-script.js
// Inject inline script into page script tag
var scriptTag = document.createElement('script')
scriptTag.src = chrome.extension.getURL('inline-script.js');
(document.head||document.documentElement).appendChild(scriptTag)
//Listen to data passed from `inline-script.js`
document.addEventListener('checkResult', function(event) {
const checkResult = event.detail
// Send data to background.js
chrome.runtime.sendMessage(checkResult)
})

Store google ads info in background.js

  • background.js
// Global variable that stores google ad info of all tabs
// Can be directly accessed in react App
const checkResult = {};
// Listen to messages from content script.
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
const tabId = sender.tab.id;
const googleTaginfo = JSON.parse(message);
checkResult[tabId] = googleTaginfo;
})
// clean storage when tab is closed
chrome.tabs.onRemoved.addListener(function(tabId, removeInfo) {
delete checkResult[tabId]
})

React.js App for extension popup

React component for showing google ad info

import React from 'react';// style sheet
import './Googletag.css';
// This is to tell compiler to ignore checking chrome global variable since it will be injected in extension environment.
/*global chrome */
export default class Googletag extends React.Component {
render() {
const googleTagInfo = this.props.googleTagInfo;
if (googleTagInfo === null || googleTagInfo === undefined) {
return;
}
var tableContent = [];
console.log(googleTagInfo);
Object.keys(googleTagInfo)
.forEach(key => {
let status;
if (googleTagInfo[key] !== null) {
status = (<div className="checkmark"></div>)
} else {
status = (<span className="crossmark">&#10060;</span>)
}
tableContent.push(<tr><td>{ key }</td><td>{ status }</td></tr>)
});
return (
<table>
<thead>
<tr>
<th scope="col">AdUnit Path</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody>{ tableContent }</tbody>
</table>
)
}
}

Wire sub-component to React app

import React from 'react';
import Googletag from './Googletag';
import {
// HistoryRouter won't work in chrome extension
BrowserRouter as Router,
Switch,
Route
} from 'react-router-dom';
import '../styles/App.css';// Claim that chrome is an external global variable and js lint should pass it
/*global chrome */
class AppComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
googletagInfo: null
}
}
componentDidMount() {
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
var tabId = tabs[0].id;
setTimeout(() => {
var googleTagInfo = chrome.extension.getBackgroundPage().checkResult[tabId];
this.setState({ googletagInfo: googletagInfo });
}, 3000)
}.bind(this))
}
render() {
return (
<div className="index">
<Router>
<div className="content-router">
<Switch>
//The path by defaut is /index.html
<Route exact path="/index.html"><Googletag googleTagInfo={ this.state.googletagInfo } /></Route>
</Switch>
</div>
</Router>
</div>
);
}
}
export default AppComponent;

Deploy the extension on the local browser

Finally, we need to add a few CSS to show checkmark and cross mark to indicate the status of each google ad.

.checkmark {
display: inline-block;
transform: rotate(45deg);
height: 30px;
width: 15px;
border-bottom: 7px solid #78b13f;
border-right: 7px solid #78b13f;
}
.crossmark {
font-size:30px
}

Refer to react-app-rewired to modify create-react-app configuration.

Add a config-overrides.js file:

module.exports = function override(config, env) {
config.optimization = {
runtimeChunk: false
}
return config;
}

We need to disable runtimeChunk otherwise the inline-scrpt.js can't be injected into the page.

npm run build

Drag the /build folder into chrome extension management page.

For testing, Navigate to some site ( like mainichi.jp) and open the extension popup.

You will see the info of google ads

The complete source code is in: https://github.com/flux-dev-team/chrome-extension-demo

If you want to see more tutorials like this, don't forget to subscribe to our publication!

--

--