Create chrome extension with ReactJs using inject page strategy

Satendra Rai
ITNEXT
Published in
8 min readJul 10, 2018

--

Chrome is an awesome browser from Google that is very fast and lightweight, yet also very powerful. Chrome also has a very good extensibility model that allows developers with just HTML, CSS, and JavaScript skills to create powerful extensions.

I will show you how to inject our own JavaScript and CSS into an existing page to enhance its capabilities.

Table of contents

  1. Creating and setting up a React application
  2. Adding React app extension to Chrome
  3. Injecting a React app to page as content script
  4. How to utilize the Chrome messaging API
  5. Isolate extension CSS using Iframe
  6. Routing inside react extension
  7. Github repo for quick starters
  8. Make extension compatible with Create React App v2.x

Creating and setting up a React application

In your command line, go to your workspace directory and run npx create-react-app my-extension. This will set up a sample React application named my-extension, with all the build steps built-in.

Once you have the basic react app created, go to my-extension directory and run yarn start to make sure the application is working fine. If all good, you will see a browser page, loaded with the React app.

Setup react app to use as an extension

Our create-react-app has manifest.json. We just need to add a few details in it to make it compatible with Chrome’s manifest.json. Open the file [PROJECT_HOME]/public/manifest.json and replace it with the following code.

{
"name": "My Extension",
"version": "1.0",
"manifest_version": 2,
"browser_action": {
"default_popup": "index.html"
},
"content_security_policy": "script-src 'self' 'sha256-GgRxrVOKNdB4LrRsVPDSbzvfdV4UqglmviH9GoBJ5jk='; object-src 'self'"
}

Adding React app extension to Chrome

To make React app working as a Chrome extension. Build this app as you normally build a react app with yarn build. This generates the app and places the files inside [PROJECT_HOME]/build.

In the Chrome browser, go to chrome://extensions page and switch on developer mode. This enables the ability to locally install a Chrome extension.

Now click on the LOAD UNPACKED and browse to [PROJECT_HOME]/build , This will install the React app as a Chrome extension.

When you click the extension icon, you will see the React app, rendered as an extension popup.

Injecting React app to page as content script

Chrome extension uses content_scripts to mention a JS and CSS file in manifest.json, that needs to be injected into the underlying page. Then this script will have access to the page DOM.

The problem with our create-react-app is that the build step will generate the output JS file in different name each time (if the content changed). So we have no way to know the actual file name of the JS file, hence we can’t mention in it in our manifest.json file.

As a workaround, you can just eject out of create-react-app and modify the webpack configuration by hand to create a separate entry point for content script.

Ejecting create-react-app and configuring content scripts

What happens when you eject create-react-app

First, run yarn run eject on the command line. This will eject create-react-appand then will create all necessary build scripts inside your project folder.

Now run yarn install to install all dependencies

Once ejection is done, go to [PROJECT_HOME]/config/webpack.config.prod.js file and make the following changes in it:

Change the option entry to have multiple entry points. Here, our content script will be named as content.js

entry: {
app: [require.resolve('./polyfills'), paths.appIndexJs],
content: [require.resolve('./polyfills'), './src/content.js']
},

Also, Search for .[contenthash:8], .[chunkhash:8] and remove it from both CSS and JS output file name. This will ensure the generated file will not have a random hash in it, thus we can mention the file name in our manifest JSON.

Once you made the above changes in the webpack.config.prod.js file, now its time to create the content script file. Create a file named content.js and content.cssinside the src folder.

/* src/content.js */
import React from 'react';
import ReactDOM from 'react-dom';
import "./content.css";

class Main extends React.Component {
render() {
return (
<div className={'my-extension'}>
<h1>Hello world - My first Extension</h1>
</div>
)
}
}

const app = document.createElement('div');
app.id = "my-extension-root";
document.body.appendChild(app);
ReactDOM.render(<Main />, app);
/* src/content.css */.my-extension {
padding: 20px;
}
.my-extension h1 {
color: #000;
}

And add below CSS to index.css , I will explain later why we kept both CSS in separate files.

/* src/index.css */#my-extension-root {
width: 400px;
height: 100%;
position: fixed;
top: 0px;
right: 0px;
z-index: 2147483647;
background-color: white;
box-shadow: 0px 0px 5px #0000009e;
}

Now that we have configured the React build pipeline and created our content scripts, lets update manifest.json to pick up these files. Add the following code to manifest.json file.

"content_scripts" : [
{
"matches": [ "<all_urls>" ],
"css": ["/static/css/app.css", "/static/css/content.css"],
"js": ["/static/js/content.js"]
}
]

Now build your app, go to chrome://extensions and reload the extension, When you go to any website and refresh it, you can see our extension injected there.

Now at this stage when you click on extension icon, you can see a popup will also appear with the same component that is inject on page, but the accepted behaviour should be, on clicking extension icon the injected page must behave as popup (toggle on click)

For this, we have to use Chrome messaging API

How to utilize the Chrome messaging API

For accessing chrome API we need to add background script inside [PROJECT_HOME/public/app/background.js and add the below code to it.

// Called when the user clicks on the browser action
chrome.browserAction.onClicked.addListener(function(tab) {
// Send a message to the active tab
chrome.tabs.query({active: true, currentWindow:true},
function(tabs) {
var activeTab = tabs[0];
chrome.tabs.sendMessage(activeTab.id,
{"message": "clicked_browser_action"}
);
});
});

The code will be executed on extension icon click, It will find the current tab and use sendMessage API of chrome tabs to broadcast message inside that tab.

Add background entry to public/manifest.json

"background": {
"scripts": ["app/background.js"]
}

and remove the default_popup key from browser_action

Note: Don’t remove browser_action key, keep it empty otherwise extension icon click won’t work

"browser_action": {}

Now we need to create a receiver that will receive a message on browser action clicked. Add below code to src/content.js file

app.style.display = "none";chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
if( request.message === "clicked_browser_action") {
toggle();
}
}
);
function toggle(){
if(app.style.display === "none"){
app.style.display = "block";
}else{
app.style.display = "none";
}
}

Note: Don’t forget to add /*global chrome*/ at the top of the React component so the build will succeed

Now build your app, go to chrome://extensions and reload the extension, When you go to any website and refresh it, On clicking extension icon, injected page will toggle

Isolate extension CSS using iframe

When you start writing styles for your component, you will find out that CSS becomes totally broken on some of the sites. So for keeping your CSS isolated, I believe the best solution today is IFrames, Everything inside an iframe will run in an isolated environment.

For that, I am using react-frame-component

Install react-frame-component using yarn add

yarn add react-frame-component

Now use Frame component to wrap your Main component.

/*global chrome*/
/* src/content.js */
import React from 'react';
import ReactDOM from 'react-dom';
import Frame, { FrameContextConsumer }from 'react-frame-component';
import "./content.css";
class Main extends React.Component {
render() {
return (
<Frame head={[<link type="text/css" rel="stylesheet" href={chrome.runtime.getURL("/static/css/content.css")} ></link>]}>
<FrameContextConsumer>
{
// Callback is invoked with iframe's window and document instances
({document, window}) => {
// Render Children
return (
<
div className={'my-extension'}>
<h1>Hello world - My first Extension</h1>
</div>
)
}
}
</FrameContextConsumer>
</Frame>
)
}
}

Note: If you want to make use of iframe document or window you can use FrameContextConsumer , you can pass it as props to child component, If it is not clear ask me in comments.

In above code you can see I have used getURL chrome API, to add content.css into head of iframe document, so that it will not affect the host page CSS.

To make getURL chrome API work we need to add content.css under web_accessible_resources key in our manifest.json and remove it from content_scripts key.

"content_scripts" : [{
"matches": [ "<all_urls>" ],
"css": ["/static/css/app.css"],
"js": ["/static/js/content.js"]
}
],
"web_accessible_resources":[
"/static/css/content.css"
]

We need to define height and width of iframe, otherwise, it will not be visible, Add below CSS in index.css

#my-extension-root iframe {
width: 100%;
height: 100%;
border: none;
}

We have kept two separate files index.css will be compiled as app.css which is used to apply styles on HTML Elements that are outside iframe and content.css will be compiled as content.css which is used to style elements that are inside iframe to prevent css leakage to host page

Now build your app, go to chrome://extensions and reload the extension, When you go to any website and refresh it. React Component is rendered inside an iframe.

Routing inside react extension

If you have more than two child components you must require navigating between them, But using react-router is a bit risky, there are some problems with this approach.

  1. when you navigate between components the components routes will be visible to the host page address bar, which will break the host page if you reload it, and hence it is not acceptable.
  2. You can look for hashLocationStrategy , but in that case, the host page browser back button will be affected.

So the solution must be something that offers a stack-based router that allows basic navigation by pushing to and popping from the router’s state and for that there is route-lite package.

I am not covering its implementation in this blog, But you can always ask me in comments if you have any queries.

Clone the git repo for quick start

Important Note

If you have any issues regarding the implementation don’t forget to check the below comments and Github issues section Maybe I have already answered your question.

(Help others to find my article on Medium by giving it a 👏🏽 below. )

--

--