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>