'Multiple re-renders when using react as content script
I'm currently learning how to develop chrome extension using React. However, i'm stuck with an issue where react re-renders like 3 times even with a very basic component.
As a result, the content script creates iframes 3 times nested on top of each other -- and one thing i've noticed is querying iframe always returns null on every re-render.
Here's the component that will act as a content script that will be injected inside a iframe.
import { render } from 'react-dom';
interface Props {}
const Sidebar: FC<Props> = () => {
return <div>some sidebar</div>;
};
// render first on the entry point
const app = document.getElementById('app');
if (app) {
render(
<StrictMode>
<Sidebar />
</StrictMode>,
document.getElementById('app')
);
}
console.log('check');
const id = 'extensionWrapper';
const iframe = document.getElementById(id);
if (!iframe) {
const iframe = document.createElement('iframe');
// get the index.html from chrome extension directory
const content = chrome.extension.getURL('index.html');
console.log({ content });
iframe.src = content;
iframe.id = id;
document.body.appendChild(iframe);
}
Here's the webpack.config.js
const { resolve } = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const tsRule = {
test: /\.tsx?$/,
exclude: /node_modules/,
use: 'ts-loader',
};
const plugins = [
new HTMLWebpackPlugin({
template: 'src/index.html',
filename: 'index.html',
chunks: ['contentScript'],
inject: 'body',
}),
new CopyWebpackPlugin({
patterns: [
{
from: 'public',
// to is relative to output folder set in output.path
to: '.',
},
],
}),
new CleanWebpackPlugin(),
];
module.exports = {
mode: 'development',
devtool: 'cheap-module-source-map',
entry: {
// popup: './src/popup-page/popup.tsx',
contentScript: './src/content-scripts/content-script.tsx',
},
output: {
filename: '[name].js',
path: resolve(__dirname, 'dist'),
},
module: {
rules: [tsRule],
},
plugins,
};
manifest.json
{
"short_name": "react chrome",
"name": "react chrome extension",
"version": "1.0",
"manifest_version": 2,
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["contentScript.js"]
}
],
"web_accessible_resources": ["*.html"]
}
Solution 1:[1]
I haven't found an answer on the multiple render, but in case someone stumbled upon in this post -- instead of specifying the content script in manifest.json
, i decided to just inject the content script dynamically using the snippet below:
Inside background.ts
const isWebProtocol = (url: string | undefined): boolean => {
return !!url && url.startsWith('http');
};
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete' && tab.active === true && isWebProtocol(tab.url)) {
chrome.tabs.executeScript(tabId, {
file: 'injectScript.js',
runAt: 'document_idle',
});
}
});
The content script that will be injected on each tab
injectScript.js
const id = 'extensionWrapper';
const iframe = document.getElementById(id);
if (!iframe) {
const iframe = document.createElement('iframe');
// get the index.html from chrome extension directory
const content = chrome.extension.getURL('index.html');
iframe.src = content;
iframe.id = id;
document.body.appendChild(iframe);
}
the main content script that initializes the main react component
contentScript.tsx
import React, { FC, StrictMode } from 'react';
import { render } from 'react-dom';
// import Sidebar from "./content-script.module.css";
import "../style.css";
import "./content-script.css";
interface Props {}
const Sidebar: FC<Props> = () => {
return <div className="sidebar">some sidebar</div>;
};
// render first on the entry point
const app = document.getElementById('app');
if (app) {
render(
<StrictMode>
<Sidebar />
</StrictMode>,
document.getElementById('app')
);
}
Lastly, the index.html
file that contains the element (i.e #app
) where the main react component will be injected to
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
Solution 2:[2]
Piggybacking on the other answer, but for Manifest V3:
// serviceWorker.ts
const isWebProtocol = (url: string | undefined): boolean => {
return !!url && url.startsWith("http");
};
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === "complete" && tab.active === true && isWebProtocol(tab.url)) {
chrome.scripting.executeScript({
files: ["sidebar.js"],
target: { tabId },
});
}
});
// sidebar.ts
const id = "extensionWrapper";
const iframe = document.getElementById(id);
if (!iframe) {
const iframe = document.createElement("iframe");
iframe.style.height = "100%";
iframe.style.width = "400px";
iframe.style.position = "fixed";
iframe.style.top = "0px";
iframe.style.right = "0px";
iframe.style.zIndex = "2147483647";
iframe.style.border = "0px";
const content = chrome.runtime.getURL("index.html");
iframe.src = content;
iframe.id = id;
document.body.appendChild(iframe);
}
// manifest.json
{
"manifest_version": 3,
"action": {},
"background": {
"service_worker": "./static/js/serviceWorker.js"
},
"permissions": ["activeTab"],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["./static/js/sidebar.js"]
}
],
"web_accessible_resources": [
{
"resources": ["index.html"],
"matches": ["https://*/*", "http://*/*"],
"extension_ids": ["???"]
}
]
}
I am using Nx and have set up a custom webpack.config.js
like so:
const HtmlWebpackPlugin = require("html-webpack-plugin");
const nrwlConfig = require("@nrwl/react/plugins/webpack.js");
const path = require("path");
module.exports = (webpackConfig, context) => {
nrwlConfig(webpackConfig);
const template = context.buildOptions?.index ?? context.options?.index;
// Remove the current html plugin added by NX
const idx = webpackConfig.plugins.findIndex((plugin) => plugin.constructor.name === "IndexHtmlWebpackPlugin");
webpackConfig.plugins.splice(idx, 1);
return {
...webpackConfig,
entry: {
...webpackConfig.entry,
sidebar: "./src/content-scripts/sidebar.ts",
serviceWorker: "./src/service-worker/serviceWorker.ts",
},
output: {
...webpackConfig.output,
filename: "static/js/[name].js",
},
optimization: {
...webpackConfig.optimization,
runtimeChunk: false,
},
plugins: [
...webpackConfig.plugins,
new HtmlWebpackPlugin({
chunks: ["polyfills", "main", "sidebar"],
filename: "index.html",
template: path.join(process.cwd(), template),
}),
],
};
};
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | The.Wolfgang.Grimmer |
Solution 2 | DharmanBot |