loading...

React (injected) Chrome extension

leibole profile image leibole ・3 min read

Intro

In this post, I'll quickly run through some very useful information on how to inject a React app into an existing web page. I've used it to extend a specific web app that had no other way of extending, but it can be useful for many other scenarios. There's also a double bonus:

  1. I'll show how to run the extension in a dev environment.
  2. We'll see how to auto-reload the extension once the code is changed.

Stage 1: Create React App

It seems like every React how to tutorial starts with this line, and so does this one. Create a new react app using Create React App. I have actually created a react app with typescript enabled:

npx create-react-app my-app --template typescript

Now we have a basic react app, with the react default content. Let's replace the contents off App.tsx with the most basic content to inject:


import React from 'react';

const App = () => {
  return <div>Some injected content</div>
}

export default App;

Stage 2: Extension manifest file

Each extension needs a manifest file (see extension manifest file). Our file should be located in the public folder, and should look something like this:

{
  "name": "Extension name",
  "version": "1.0",
  "manifest_version": 2,
  "browser_action": {
    "default_popup": "index.html"
  },
  "content_security_policy": "script-src 'self' 'sha256-<the extension hash>'; object-src 'self'",
  "background": { "scripts": ["hot-reload.js"] }, // see bonus :)
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "css": ["/static/css/main.css"],
      "js": ["/static/js/main.js"]
    }
  ]
}

Stage 3: Eject Create React App

I always prefer to avoid ejecting a Create React App (CRA) project, but we have to in this case. We want the output files to always be named main.js and main.css and avoid the random hash in the file name that's used by default in CRA. So let's run

npm run eject

We need to edit the webpack.config.js file: we need to remove the "chunkhash" from the output file names, both main.js and main.css.
We can now run

npm run build

and get the built files output. One thing is still missing: the actual injection code.

Stage 4: Injecting the React App

Now usually in a normal React App, we'll create a

<div id="root></div>

inside the index.html file, and then call

ReactDOM.render(<App />, document.getElementById("root"));

to insert the app.
The injection code is very similar: we choose where to inject the app (for example - the body element), and append a div to it with the id "root":

const app = document.createElement("div");
app.id = "root";
document.body.append(app);
ReactDOM.render(
  <App />,
  document.getElementById("root")
);

And just like that, the React App is appended to the body.

Bonus #1: Run in Dev mode

The basic usage of the app is now as an injected div. But in that case, whenever we make a change we have to reload the extension, and even worse, the code is the built code, uglified and minified and unreadable. What can we do in development?
Just have the app inject itself as a normal React app. include the normal root code in the index.html file, and in index.tsx check if the environment is development, and if so attach the React app to the root element:

if (process.env.NODE_ENV === "development") {
  ReactDOM.render(
    <App />,
    document.getElementById("root")
  );
} else {
  const app = document.createElement("div");
  app.id = "root";
  document.body.append(app);
  ReactDOM.render(
    <App />,
    document.getElementById("root")
  );
}

Bunos #2: Auto reload the extension on file changes

To avoid having to manually reload the extension on each rebuild, I've used a hot-reload code snippet I found in this repo https://github.com/xpl/crx-hotreload. Just copy the file hot-reload.js from this repo into your public folder, and include this line in the manifest file (already included in the example manifest file above):

"background": { "scripts": ["hot-reload.js"] }

The file from the repo needs a small change to work well with the React ecosystem: a short timeout (~10 seconds) needs to be added before reloading the extension, to allow for the build to complete.

Good luck, you're welcome to comment if you have any questions.

Discussion

pic
Editor guide