Table of content
- Chrome Extensions Structure
- Chrome Extensions + React (CRA)
- The main problem of Chrome Extensions and CRA
- Conclusion
Chrome extensions have become a popular way to extend the functionality of many web apps by making them easier to use and more accessible. However, despite how common they are, building them is not so straightforward, especially when you are using a more complex stack with modern web frameworks such as React.
Chrome Extensions Structure
To understand how to build chrome extensions with React (specifically CRA), we first need to grasp the typical structure of a Chrome extension. An extension consists of these elements: background script, content scripts, popup (html + js), manifest.json.
- Background script – the extension’s core, which runs in its own environment (or ‘context’). It’s active and running as long as your browser is open and the extension is enabled.
- Content scripts – scripts that are injected into websites and run in their context. They are written using JavaScript and hence can do whatever JS can – change the styles, inject html, send requests etc. We can control whether they run in all websites you visit, or only on specific URLs.
- popup.html is the html file that is opened by default in the popup window when you click on the extension’s icon. Like any html file, it can have CSS and JavaScript attached to it.
- manifest.json – the main source of truth for the browser, here is where all the specifications of our extension are, such as name, version, icon etc. Here is also where we write the paths to our content and background scripts, and where the main problem with React and CRA occurs. We will get to this next.
Chrome Extensions + React (CRA)
Let’s start by creating a new CRA project. We will build an extension that injects a popup onto a webpage on the click of a button inside the extension’s main popup.
npx create-react-app chrome-extensions-react-demo
In order to run a React app both in the main popup and the current webpage, we will need to technically create two React applications: one that gets rendered inside the popup.html file, and one in the context of the webpage.
Let’s start with the popup.html. To get this part to work, we just have to ensure CRA’s index.html is correctly added in the manifest.json in the ‘default_popup’ field. I have renamed it to popup.html to clearly distinguish it.
We also need to edit the manifest.json to fit the chrome extension structure. We’ll add a manifest version property to the file with the value ‘2’ (the currently supported version), the app version, and icons.
Create React App by default adds an inline script to the html file, which in chrome extensions will cause errors in the console. To disable this behaviour, we have to add this line in the .env file:
INLINE_RUNTIME_CHUNK=false
Let’s adjust the popup’s styling since it looks a bit wonky with the default styles. We need to keep in mind that the maximum size of the popup window is 600px height by 800px width.
Now, to preview our application we need to run the build command and load the build folder into the browser on the extensions panel. Unfortunately, we will need to redo this every time we make a change in the app.
npm run build
This command will have added a /build directory in your app’s main directory.
Go to chrome://extensions/ in your browser, turn on ‘Developer mode’ and click load unpacked. Navigate to your app’s folder and select the /build directory.
We can now see the app in the taskbar.
Now go to any website and click on the icon. The popup should open with the default CRA app. Hooray!
Now we want to add to our app the ability to inject React code into pages. To do that, we will create a second React app to inject into visited pages. Let’s restructure the app a bit to easily separate the two React apps by putting our existing files into src/popup.
Next let’s add a contentScript directory under /src, and create two files in there: index.js – where we will be mounting the app into the DOM, and ContentScript.js which will be the main, top-level React component.
To ensure that the injected app’s scripts and styles are isolated and do not conflict with the current webpage’s, we will wrap it in a web component. In the index file, instead of mounting the app component on the div with the id of ‘root’, we will pack the app into a web component and mount it as a direct child of the webpage’s html document. For this to work you will need to install @webcomponents/custom-elements.
npm install @webcomponents/custom-elements
For the modal component I will be using MaterialUI with JSS, since they both work well in the web component shadow DOM environment and don’t require much extra reconfiguration.
This is our initial code for mounting the web component wrapper for the content script React application.
Let’s now add the actual ContentScript modal that will be injected into the page.
Let’s run build again to update our build files.
npm run build
To make sure the app content script is run by the browser, we need to add it in the manifest.json as a content script with a specific path. But if we look into the build folder we previously generated, we will see that there is no main javascript file to load, only a bunch of chunks and we can’t tell which is the popup app and which is the injected app.
The main problem of Chrome Extensions and CRA
As we know, CRA is built using webpack, which is configured to bundle our application’s code and generate chunks of JavaScript which are then loaded by the application. This way of chunking code results in randomly generated files whose names change every time we run the build command. Let’s recall the structure of chrome extensions: we have content and background scripts, and a manifest.json. In the manifest, we pass paths to the scripts to tell the browser which files it should load. The problem is, the manifest is a static json file so we have no way to dynamically tell the names of the JavaScript chunks we want to load. To fix this problem, we need to reconfigure webpack in a way that it will generate JavaScript files with non-changing names. And to do that we need to eject out of CRA.
npm run eject
This command will create a /config directory and there expose webpack configuration files to us.
We need to change two files, ‘paths.js’ and ‘webpack.config.js’ to disable chunking and tell webpack what to name the files.
Whew, that was quite a bit!
This way we can control how and where our JavaScript is output, so we can keep our file paths static.
The last thing we have to do now is add the new static js file paths to our content script field in the manifest.json and the related permissions shown below.
We can now build our React application and run it as a content script. Yay!
Let’s add one more thing: a background script.
To do that, we’ll follow the previous steps of adding a new subfolder src/background with an index.js file for our background script to live in.
To allow communication between all the different extension parts and scripts, the chrome extension API provides a messaging functionality. We can send a message from the content script to the background script, execute some logic in the background script, and send (or not) a response back to the content script.
We can then intercept the message within the content script and for example display it in the frontend.
We’ll have to adjust the webpack configuration to exclude the background script from chunking too.
The difference between a background script and a context script is that content scripts run within the context of a webpage in one tab – so, if you have many tabs open, you might be running multiple instances of the content script, one in each tab. The background script on the other hand runs as one background process in the browser, no matter how many tabs you have open.
Conclusion
Building chrome extensions with React can be tricky and difficult to debug, but we have tackled the main problems successfully. We have managed to build a chrome extension using React by doing one major thing: ejecting out of CRA and changing the webpack configuration. Of course the solution given here is just one of many – you’re welcome to go ahead and adjust it to suit your needs specifically.
After you are done with building and testing your application, you can publish the extension on Chrome Web Store, where it will undergo an official review before going live.
Project code: https://github.com/teacodeio/chrome-extension-article