How to access React Props from Chrome Extension

TL;DR Version

  1. React props can be accessed using Native JS, using the following logic:
    reactProps = element[ Object.keys(element).filter(k => k.includes("Props"))[0] ]
  2. However, this cannot be accessed via a Chrome Extension because the extension content scripts run in an isolated world.
  3. To overcome this shortcoming, just add the following object to your content_scripts array in manifest.json:
    {
      "world": "MAIN",
      "js": ["src/main.js"],
      "css": ["src/main.css"],
      "matches": ["https://example.com/*"],
      "run_at": "document_end"
    }

The Long Version

I do a fair bit of Chrome Extension development as a part of my job owing to which I come across problems that usually require me to think outside the box. One such instance occurred when I was faced with the issue of obtaining data for objects in the DOM. Now, the data that I was looking to read was only shown to the user via a tooltip when the user hovered over an element. The tooltip HTML wasn’t present in the DOM, rather it was built and added dynamically to the page when the user hovered over the element. And I wasn’t just supposed to fetch this data for one element, but for hundreds of elements that were present in the DOM.

Now, a crude approach to solve this problem was to make an API request that was fetching this data from the backend. But, I knew that the query is quite heavy and I did not want to unnecessarily over burden the server with duplicate API calls. I knew that the data is embedded somewhere in the page’s memory. I knew that the frontend is written in React, so the data should be in the props or state of one of the components on the page. So, how did I solve it?

Turns out React saves a component’s props in the DOM Element’s properties. No kidding. Just try this on any react website that you may have access to:

Inspect any element on the DOM using the browser’s inspect feature. Then go to the console and run the following code:

$0[Object.keys($0).filter( k => k.includes("Props") )[0]]

Below is an example from `hxxps://teampassword.com/`:

OK great, so we have a way to fetch the React Props using Javascript. Now, I tried to fetch the same props using the Chrome Extension. However, to my surprise the DOM Element Object did not show the React props property when trying to access it using the extension content script. I began googling as to why I was not able to access something from the extension which was easily accessible using native JS. But Why?

Because of the Isolated World concept:

Content scripts live in an isolated world, allowing a content script to make changes to its JavaScript environment without conflicting with the page or other extensions’ content scripts.

An isolated world is a private execution environment that isn’t accessible to the page or other extensions. A practical consequence of this isolation is that JavaScript variables in an extension’s content scripts are not visible to the host page or other extensions’ content scripts. The concept was originally introduced with the initial launch of Chrome, providing isolation for browser tabs.

https://developer.chrome.com/docs/extensions/mv3/content_scripts/#isolated_world

So, how did I solve it?

Well, I thought the workaround for this was going to be rather complex and hacky. But I came across a rather detailed StackOverflow answer from a good samaritan and my life was sorted. There is a way to tell Chrome to run our extension content script in context of the page instead of running it in an isolated world. It’s quite simple:

In your manifest.json, add an object to the content_scripts array with the following properties:

    {
      "world": "MAIN",
      "js": ["src/main.js"],
      "css": ["src/main.css"],
      "matches": ["https://example.com/*"],
      "run_at": "document_end"
    }

The "world":"MAIN: property tells chrome to run the JS in the main world instead of the isolated world and the "run_at": "document_end" property tells Chrome to run the content script only after the DOM is completely loaded. Once this is done, you can access React props using the logic explained above. I am going to repeat it for the sake of continuity:

let element = document.querySelector(".selector")
let reactProps = element[ Object.keys(element).filter(k => k.includes("Props"))[0] ]
// You can then access the data you are interested in by inspecting the props and identifying the path to the data you are interested in