Creating a Chrome Extension with React

Nil Seri
9 min readDec 23, 2021

--

Develop a Chrome Extension with React, Redux and Bootstrap

Photo by Ardi Evans on Unsplash

TL;DR

You can choose which front-end language you would like to use for your project. I preferred to code with React and used React Bootstrap as a CSS framework. For icons, I used Font Awesome.

If you are not interested in React related details, you can directly skip to “Chrome Related Details” section.

React Related Details:

You can install React Bootstrap (please check the official site for latest version)

yarn add react-bootstrap bootstrap@5.1.3

I preferred to import components individually to my pages:

import { InputGroup, FormControl, Button } from 'react-bootstrap';

You can check components and how you can use them with specific styling options here from under Components.

For toasters, I used “react-toastify”. You can see its official demo page here and try some combination of different styles.

yarn add react-toastify

For Font Awesome, instructions for usage in React is provided here.

yarn add @fortawesome/fontawesome-svg-core
yarn add @fortawesome/free-solid-svg-icons
yarn add @fortawesome/react-fontawesome
yarn add @fortawesome/free-brands-svg-icons
yarn add @fortawesome/free-regular-svg-icons

Since this is a smaller project compared to other web projects, I preferred to “Add Individual Icons Explicitly” as you can find under the same title name in the official docs here.

import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCalendar, faEnvelope } from '@fortawesome/free-solid-svg-icons';
import { faSmile } from '@fortawesome/free-regular-svg-icons';
...<FontAwesomeIcon icon={faSmile} />

For localization, I used “react-i18next” package and followed the instructions from here:

yarn add react-i18next i18next

Usage:

import { useTranslation } from 'react-i18next';...
const { t } = useTranslation();
...
<span className="me-1">
{t('WELCOME')}{' '}
{props.authedUser.name
? props.authedUser.name
: props.authedUser.client_id}
</span>

As a requirement specific to this project, I added “react-linkify” to my dependencies. You provide a text to linkify and it parses links (urls, emails, etc.) in text into clickable links. This is useful for calendar events where location info is converted into clickable urls so that the extension opens a new tab routing to that link when clicked.

yarn add linkifyjs linkify-react

You can read details for usage in React, here.

import Linkify from 'linkify-react';...const linkProps = {
onClick: (event) => {
chrome.tabs.create({
url: event.target.href
});
}
};
...<Linkify options={{ attributes: linkProps }}>
{event.location}
</Linkify>

I needed “env-cmd” package to handle my custom environment property files in the project. In this project, there are two environments; “individual” and “enterprise” for e-mail account types.

yarn add env-cmd

I created “environments” folder at the same level with package.json file and added two files named “.env.enterprise” and “env.individual”. You should start your property names with “REACT_APP_”.

Later on, I added these configurations under “scripts” section of package.json:

"build:i": "env-cmd -f ./environments/.env.individual npm run-script build",
"build:e": "env-cmd -f ./environments/.env.enterprise npm run-script build"

For handling api requests, I used “axios” package:

yarn add axios

I created an “AxiosConfig.js” file to set baseUrl from the related environment file and also add an interceptor to handle 401-Unauthorized status code and other error code handling and returning a custom user friendly message in account’s language.

import axios from 'axios';
import i18n from 'i18next';
import UtilsService from '../services/UtilsService';
import { logout } from '../actions/authedUser';
import { useDispatch } from 'react-redux';
const createErrorMessage = (error) => {
let errorCode = error?.error?.message;
if (errorCode && i18n.exists(errorCode)) {
return i18n.t(errorCode);
} else {
return error.error.message;
}
};
const API_CONFIG = axios.create({
baseURL: process.env.REACT_APP_GATEWAY_API_URL
});
API_CONFIG.interceptors.response.use(
(response) => {
return response;
},
(error) => {
if (error.response.status === 401) {
return new Promise((resolve, reject) => {
UtilsService.saveToLocalStorage('ym@user', null)
.then((response) => {
useDispatch(logout());
resolve(response);
})
.catch((error) => {
// console.log(error);
reject(error);
});
});
} else {
error.response.message = createErrorMessage(error.response);
return Promise.reject(error);
}
}
);
export { API_CONFIG };

For handling app state, I used “Redux”. I did not write a custom logic to use with “Redux Thunk” but I installed it just in case for future needs.

yarn add redux react-redux
yarn add redux-thunk

I created actions, middleware and reducers folders and related code for state operations. I will cut it short and share my code at the end of this post instead if you would like to see more details 😊.

Of course, one of the essentials, “moment” library is used for formatting dates of mails and calendar events.

yarn add moment

Since localization exists, you should include related language file (this is not required for default “en” language) if your format includes non-numeric month or days.

import moment from 'moment/moment.js';
import 'moment/locale/tr';
...<div>{moment(new Date()).format('DD-MM-YYYY dddd')}</div>

Chrome Related Details

Chrome Extension Manifest v3 is used for this project as it is the latest version as of now.

Below is the official docs where you can get details for chrome API usage.

I was confused from time to time because of the version differences in the examples I came across. I was trying to implement in the same way but there was no warning about version incompatible implementations. Here is a useful link describing v2 and v3 differences from the migration point of view but you can also use them like me, if you find perfect example code which apparently uses the older version.

“manifest.json” File:

The first file you will be working on is under “public” folder, named as manifest.json.

Here is an example:

{
"name": "My Extension",
"description": "My Extension",
"version": "1.0",
"manifest_version": 3,
"keywords": ["react", "browser extension", "chrome extension"],
"permissions": [
"http://api.yoursite.com/*",
"storage",
"notifications",
"alarms"
],
"action": {
"default_popup": "index.html",
"default_title": "Open the popup"
},
"background": {
"service_worker": "background.js"
},
"icons": {
"16": "logo-minimized.png",
"48": "logo-minimized.png",
"128": "logo-minimized.png"
}
}

Chrome Extension Manifest version exists in “manifest_version”.

Permissions are required for your interactions from your extension. If you are calling an API, you should add its url there. If you are using localstorage for your extension (I will go into more detail about this, later in this post), you should declare it there. If you plan to set a badge (notifications) on top of your extension icon and if you are refreshing it (using an alarm, triggering after a configurated time repeatedly), this is the place to declare.

Icons which you should add somewhere under “public” folder will be used as your extension’s icon. “index.html” will be the main file that will open when you click on your extension’s icon.

“background.js” will include Javascript code that you wish to run in the background (I will give more details about this file in the upcoming sections).

Chrome Storage:

For security reasons, you only have access to your extension’s localstorage. Its usage is different from the ordinary one.

I used “chrome.storage.sync” because I wanted my extension to sync its data of the owner account user from different Chrome browsers (if sync is enabled). You can also use “chrome.storage.local”, too (if you do not plan to use “sync” option).

Reaching (or setting) “chrome.storage.sync” requires an async operation as stated in official docs:

Unless the doc says otherwise, methods in the chrome.* APIs are asynchronous

I created a separate service file to handle chrome.storage operations and created utility functions to get and set:

static saveToLocalStorage = (key, value) => {
return new Promise((resolve, reject) => {
var obj = {};
obj[key] = value;
chrome.storage.sync.set(obj, () => {
resolve(value);
});
});
};
static getFromLocalStorage = (key) => {
return new Promise((resolve, reject) => {
chrome.storage.sync.get(key, (data) => {
if (data) {
resolve(data);
} else {
reject();
}
});
});
};

You can also set/get multiple keys’ values at one time by calling like:

chrome.storage.sync.set({
'ym@user': JSON.stringify(response.data),
'ym@view': 'inbox'
}, () => {
...
chrome.storage.sync.get(['ym@user', 'ym@view'], (data) => {
...

“INLINE_RUNTIME_CHUNK=false” Build Parameter

For Chrome Extension build, you need to add “INLINE_RUNTIME_CHUNK=false” as a build parameter in your React project.

Here in official React docs, you can read about advanced configuration. For this config:

By default, Create React App will embed the runtime script into index.html during the production build. When set to false, the script will not be embedded and will be imported as usual. This is normally required when dealing with CSP.

I added this parameter in build config under “scripts” in package.json file:

"build": "INLINE_RUNTIME_CHUNK=false react-scripts build",

Background Operations:

You should add your background.js file under “public” folder.

"background": {
"service_worker": "background.js"
},

There is a pretty good example of a background.js file under title “4. Fetch data and save to localStorage” which I also referred:

Be aware that manifest version is 2 in the example above so it was declared as below. This usage is updated as “service_worker” in v3.

"background": {    
"scripts": ["background.js"],
"persistent": false
}

Open a Link In a New Tab:

As you can see in the example code I shared for “linkify-react” usage, you can provide the url you want Chrome to open in a new tab.

const handleRouteToUrl = () => {
let yourUrl = "www.your_url_will_go_here.com"
chrome.tabs.create(
{
url: yourUrl
}, function (tab) {
...
}
);
};

I did not add “tabs” under “permissions” in our manifest.json file because I did not need to access any current user info from browser.

Messaging within Your Extension:

The extension shows a badge for the count of unread e-mails in the account’s inbox. This count value is updated each time an API request is performed via background.js. There is also a button labelled “Unread” which also shows a badge within the button showing the count of unread mails and when you click, you are routed to the original website.

I thought, what if I pass this data from background.js to the “Unread” button in my page. I was mistaken because I started getting “Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist.” errors. Then, it became clear; the connection is closed when extension’s window is closed (just like closing a tab).

Still, since I learned how to implement, I will share here.

Adding a listener:

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request && request.type === 'REFRESH_UNREAD_COUNT_BADGE') {
sendResponse({ count: `${inbox[0].unread}` });
}
});

Sending a message:

chrome.runtime.sendMessage(
{ type: 'REFRESH_UNREAD_COUNT_BADGE' },
(response) => {
if (response) chrome.action.setBadgeText({ text: response.count });
}
);

Notifications:

I haven’t implemented it yet but I am planning to add notifications (do not forget to add it under “permissions” in manifest.json). We have a server-sent events API that provides all mailbox, contacts, folder, calendar events of the related account on the fly. I may use this for creating new notifications for user:

chrome.notifications.create('123', {
type: 'basic',
iconUrl: 'path',
title: 'notification title',
message: 'notification message',
priority: 2
});

Local Installation

Chrome’s Developer Mode:

To be able to load your project (after build) to Chrome, you need to enable “Developer mode” on your Chrome under Extensions.

After that, three buttons appear on the left. By clicking “Load unpacked” button, you are now allowed to select your project’s “build” folder.

Now, under “extensions” (chrome://extensions/), you should be able to see your extension. You can also pin it to keep it in the toolbar.

As you can see, if an extension has a background.js running, you can understand it from “service worker” or “background page” info, placed under ID.

Chrome Browser — Developer Mode

This is an ongoing project. There are still places to refactor (as well as removing console.logs 😊) and other features I would like to add. Still, I wanted to share my code hoping it would be a starting point for you 😊.

I also have to state that this is an unofficial personal side project.

Some screenshots:

Happy Coding!

--

--

Nil Seri
Nil Seri

Written by Nil Seri

I would love to change the world, but they won’t give me the source code | coding 👩🏻‍💻 | coffee ☕️ | jazz 🎷 | anime 🐲 | books 📚 | drawing 🎨

No responses yet