2

I have a simple React component injected to the DOM as content script in my Chrome Extension, running on a website which is not mine.

I want to update (re-render) the component on every navigation (url change) made by the user on the website which my React app is injected to.
I figured React Router should be the best tool for this job.
However, it doesn't seem to work. I found some related questions: this, this, this but none of the solutions work for me.

I tried to add a unique key to the Route, (like using useLocation or simply using location.href or even a Math.random() for the test) - but none of those make the component update.

The weird thing is that if I attach an onclick that changes a state to the div in my component, it does successfully makes the component re-render and update.

My content script looks like this:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

const myDiv = document.createElement('div');
myDiv.id = 'my-id';
document.body.appendChild(myDiv);

ReactDOM.render(<App />, myDiv);

My Routes (react-router v6):

import {
    BrowserRouter,
    Routes,
    Route,
    useParams,
    useLocation
} from "react-router-dom";

function App() {
    return (
        <BrowserRouter>
            <Routes>
                <Route path="/user/:id/" element={<MyComponent />} />
            </Routes>
        </BrowserRouter>
    );
}

My component:

function MyComponent(props) {
    const { id } = useParams();

    React.useEffect(() => {
       // Some stuff
    }, [id])

    return <div> User {id}</div>

My manifest.json scripts look like:

  "background": {
    "scripts": [
      "build/background.js"
    ]
  },
  "content_scripts": [
    {
      "matches": [
        "https://example-website.com/*"
      ],
      "js": [
        "build/content.js"
      ],
      "css": [
        "build/css/content.css"
      ]
    },
    {
      "matches": [
        "https://example-website.com/*"
      ],
      "js": [
        "build/reactApp.js"
      ],
      "css": [
        "build/css/content.css"
      ]
    }
  ],
amiregelz
  • 1,833
  • 7
  • 25
  • 46
  • what do you mean it does not "re-render"? wehen you enter a new url and click enter the whole page gets reloaded and render the new id. because you are not redirecting via code – MWO Feb 06 '22 at 17:10
  • I want it to re-render when a user navigates to another page, i.e every time the url changes – amiregelz Feb 06 '22 at 17:23
  • @amiregelz so you want your router to work when the user on a given website that you've injected a content script in browses to another page? If you own the website (`my-website`) then why are you doing it? It's possible but a lot trickier than you're expecting due to you not understanding how react routers work and also security restraints on content scripts – Dominic Feb 08 '22 at 19:22
  • @Dominic I don't own the website, maybe I should rename it in the example to `a-website` :) – amiregelz Feb 08 '22 at 19:39
  • @amiregelz what does your manifest look like ? – Khez Feb 08 '22 at 22:27
  • @Khez Edited and included it in the question – amiregelz Feb 09 '22 at 09:10
  • Based on your manifest, it looks like you're targeting the same domain. By user navigation, do you mean actual browser redirects or SPA changes? You might require a solution similar to https://stackoverflow.com/a/38965945/688411 for a SPA website. Otherwise your content script are already re-injected and the reactapp re-rendered on user navigation. – Khez Feb 10 '22 at 16:01

1 Answers1

0

I want to update (re-render) the component on every navigation (url change) made by the user on the website which my React app is injected to.

The answer to your question depends entirely on the precise definition of what "every navigation (url change)" means in terms of code. It sounds like you're not quite sure about this (but if you are, please update the question with this information).

When there is a page reload event to a matching URL, your app is already being injected again, so that's covered.

If the site is a single-page-application, and is modifying its content without a page reload (e.g. React), then it depends on what happens when the view data (and URL) changes.

There are some window events that you can listen for and re-render in response to: hashchange and popstate. The least performant solution is simply to poll: if no other option works, this is your last resort.

Here's how to change your content script:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

const myDiv = document.createElement('div');
myDiv.id = 'my-id';
document.body.appendChild(myDiv);

const render = () => ReactDOM.render(<App />, myDiv);

render();

const handleLocationUpdates = () => {
  window.addEventListener("hashchange", render);
  window.addEventListener("popstate", render);

  // or, if neither of those work, you might have to poll:

  // const aDelayValueThatWorksForYou = 500;
  // setInterval(render, aDelayValueThatWorksForYou);
};

handleLocationUpdates();

Note: To get the ID, you're parsing a part of the URL pathname using the useParams hook from React Router. I've never tested re-rendering an app using React Router above the Router component itself. My guess is that it'll subscribe to changes on such a re-render, but I don't know about this behavior. If not, you'll need to manually parse the ID from window.location.pathname in your app rather than using the useParams hook.


Edit

Because you haven't shared the URL of the site that you are injecting your extension into (in order to reproduce the issue), I have created a self-contained example which you can run on a local web server to see working:

Ref: MDN: Proxy/handler.apply()

example.html:

<!doctype html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Example</title>

  <script type="module">
    // Imagine that this is the JS in the example website (that you don't control)
    const delay = (ms) => new Promise(res => setTimeout(res, ms));
    const updateUrl = (relativePath) => window.history.pushState({}, '', new URL(relativePath, window.location.href));

    await delay(1500);
    updateUrl('/user/2');

    await delay(1500);
    updateUrl('/about');

    await delay(1500);
    updateUrl('/user/3');

  </script>

  <!-- This examples uses UMD modules instead of ESM or bundling -->
  <script src="https://unpkg.com/react@17.0.2/umd/react.development.js"></script><script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js"></script><script src="https://unpkg.com/@babel/standalone@7.17.2/babel.min.js"></script>
  <script type="text/babel" data-type="module" data-presets="env,react">
    // This is your entire React app

    const {useEffect, useState} = React;

    function parseId () {
      const idRegex = /^\/user\/(?<id>[^?/#]+)/;
      const id = window.location.pathname.match(idRegex)?.groups?.id;
      return id;
    }

    function useId () {
      const [id, setId] = useState();

      useEffect(() => {
        const parsed = parseId();
        if (parsed !== id) setId(parsed);
      });

      return id;
    }

    function IDComponent () {
      const id = useId();

      useEffect(() => {
        if (!id) return; // No ID in current location
        console.log('New ID:', id);
      }, [id]);

      return id ?
        (<div>User ID: {id}</div>)
        : (<div>User ID not found</div>);
    }

    function App () {
      return (
        <div>
          <h1>Detecting ID changes...</h1>
          <IDComponent />
        </div>
      );
    }

    // The logic of the body of this function would be in your content script
    function main () {
      const reactRoot = document.body.appendChild(document.createElement('div'));
      const render = () => ReactDOM.render(<App />, reactRoot);

      const applyCallback = (object, method, callback) => {
        object[method] = new Proxy(object[method], {
          apply: (target, thisArg, args) => {
            callback();
            return target.apply(thisArg, args);
          },
        });
      };

      const handleLocationUpdates = (usePolling) => {
        if (usePolling) {
          const aDelayValueThatWorksForYou = 500;
          setInterval(render, aDelayValueThatWorksForYou);
          return;
        }
        window.addEventListener('hashchange', render);
        window.addEventListener('popstate', render);
        applyCallback(window.history, 'pushState', render);
        applyCallback(window.history, 'replaceState', render);
      };

      render();

      // If the event listeners and proxy interceptor don't handle the
      // "navigation" events created by the page you're injecting into,
      // then you'll need to set the polling option to `true`
      handleLocationUpdates(false);
    }

    main();
  </script>
</head>

<body></body>
</html>

jsejcksn
  • 27,667
  • 4
  • 38
  • 62
  • It seems that the website I'm running on is built with Vue. I tried your snippet but it didn't seem to trigger any re-render. I want to detect when a user navigates from `website.com/user/1` to `website.com/user/2`. I tried to set the second content script to match both `website.com/*` and `website.com/user/*` - but in both cases, the content script doesn't seem to get triggered or updated in said navigation. That's why I tried to solve it with React Router but it didn't seem to work as well. – amiregelz Feb 11 '22 at 17:02
  • @amiregelz In your comment, you didn't specify whether you tried using the polling method or not. In either case, I've provided an updated example which doesn't use React Router, but instead includes a custom hook to parse the ID. The new example also includes a proxy interceptor for a couple of `window.history` methods (which should hopefully address the navigation behavior in whatever page you're targeting). If it doesn't, the polling option _will work_: I tested both methods. – jsejcksn Feb 11 '22 at 23:25
  • @amiregelz Did my updated answer work for you? – jsejcksn Feb 22 '22 at 22:21
  • Unfortunately no :( I'm thinking maybe I should use Vue with vue router to detect it since the app I'm injected to is written in Vue – amiregelz Mar 05 '22 at 21:35