1

I am trying to make a chrome extension and would love to use React, but after a full week of running into dead ends this seems to not be the ideal route. Thus, I decided to ask for a few suggestions here from you fine SO users as a last ditch effort before simply using Vanilla JS.

Here is my public/manifest.json file (I manually change "js":[] field to match the build folder's name for now):

{
  "manifest_version": 2,
  "name": "Name",
  "version": "0.1.0",

  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["/static/js/main.356978b8.chunk.js"]
    }
  ],

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

  "permissions": ["tabs", "http://*/"],

  "browser_action": {
    "default_title": "Title"
  }
}

My thinking is that the content script would be all the React function component files and the overall index.js using the above.

Here is package.json:

{
  "name": "client",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "^17.0.1",
    "react-dom": "^17.0.1",
    "react-scripts": "4.0.0"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ],
    "env": {
      "webextensions": true
    }
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

Here is my folder structure:

Folder Structure

src folder:

// src/App.js
function App() {
  return <div>Hello</div>;
}

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  console.log(request.tabs);
});

export default App;

// src/index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

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

public folder:

// public/background.js
chrome.browserAction.onClicked.addListener((tab) => {
  chrome.tabs.create(
    {
      windowId: null,
      url: "http://localhost:3000",
      active: true,
      openerTabId: tab.id,
    },
    (newTab) => {

      // wait for tab to load, then send message with tabs
      chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
        if (changeInfo.status == "complete" && tabId == newTab.id) {
          chrome.tabs.getAllInWindow(null, (tabs) => {
            chrome.tabs.sendMessage(newTab.id, { tabs });
          });
        }
      });
    }
  );
});

// public/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />

    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />

    <title>Title</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

My background.js simply opens a new tab to http://localhost:3000 when the extension button is clicked with chrome.browserAction.onClicked.addListener((tab)=>{...}). This works fine.

Adding the build folder to Load Unpacked in Chrome Extensions produces the extension, but when I click on the button and the new tab is created, I get

TypeError: Cannot read property 'addListener' of undefined

I know that npm run eject can be used to automate/set the filename in /static/js/..., but for now I am fine with simply changing it on every build change.

Any clues or ideas would be much appreciated!

2
  • Please provide a Minimal, Complete, and Reproducible code example. Please also share any code/file content/error messages/etc... as formatted code snippets versus images. Commented Oct 29, 2020 at 4:48
  • @DrewReese Thank you for the comment. I updated the description as per your request, please kindly remove the "close" vote if it was yours. Commented Oct 29, 2020 at 5:10

1 Answer 1

1

The problem is that your extension uses two copies of the same React app JS file ("static/js/main.*.chunk.js") that run in different contexts. One copy runs as a content script injected into active page and this one should run without errors. But there is also another copy that runs within CRA's dev server (localhost:3000) you open in a new tab. And this one runs as ordinary JS file without any connection to your extension, so it cannot call Chrome API. That's why you get that error.

A possible solution would be to split that JS file into two separate parts: one (ordinary JS related to index.html) - to do React rendering only, another (content script) - to listen messages from extension. Communication between such two parts can be done using window.postMessage. In your case something like below:

public/manifest.json:

{
...
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["contentScript.js"]
    }
  ],
...
}

public/contentScript.js:

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  window.postMessage({ type: "FROM_EXT", tabs: request.tabs }, "http://localhost:3000");
});

src/index.js:

...
window.addEventListener("message", event => {
  if (event.source != window)
    return;
  const {type, tabs} = event.data;
  if (type !== "FROM_EXT")
    return;
  console.log(tabs);
});

Furthermore I'd suggest you to use custom CRA template - complex-browserext (plug). It facilitates usage of Create React App with browser extensions. Also see this article for particular usage example (another plug).

Sign up to request clarification or add additional context in comments.

4 Comments

First off all thank you for the detailed response. I attempted splitting the JS file as you mentioned but could not get it to work. Is there a chance that you could provide a basic example of this file?
Note that you cannot load/open localhost URL in production. You have to change your background script to open page within extension (using runtime.getURL).
Again thank you for the useful information. Not sure why you deleted my previous comment though.
@lbragile I didn't delete your comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.