I'm trying to incorporate React Router into my Firefox browser extension to be able to display different pages in the extension's popup window. However, I keep getting the warning, You should call navigate() in a React.useEffect(), not when your component is first rendered., which causes React Router to ignore the navigation attempt.
The thing is, I'm not directly calling navigate. I'm using React Router's provided Link component, in a manner that seems to me to be entirely consistent with the doucmentation. Notably, much like in my own implementation, the documentation suggests that using Link eliminates any need to use useEffect or listen for a change in state.
In an effort to bypass this, I did try emulating a fix found for a similar issue, described here, where they used a toy state prop to make sure that no navigation occurred until the second time rendering the component. However, this didn't change the outcome at all. Below is the relevant code.
How could I fix my code to allow the browser extension to navigate within the popup window? Is something wrong with my implementation, or is this a limitation of Firefox extensions that prevents navigation with React Router?
Any and all assistance would be much appreciated!
import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom"
import { StaticRouter as Router, Routes, Route, Link } from "react-router";
import Button, { ButtonType } from "./ui/widgets/Button.jsx";
/* ********* *
* CONSTANTS *
*************/
const PATH_ROOT = "/";
const PATH_OPTIONS = "/options"
/* **************** *
* REACT COMPONENTS *
********************/
function MainMenu() {
const [rendered, setRendered] = useState(false);
console.log("Rendering...");
const menu = <div className="mainMenu">
<Link to={PATH_OPTIONS}><Button>Options</Button></Link>
</div>
useEffect(() => {
setRendered(true);
console.log("Rendered!");
}, [])
return menu;
}
function OptionsMenu() {
console.log("Attempting to render OptionsMenu");
return <div>
<h1>Options</h1>
<Link to={PATH_ROOT}>
<Button>Back</Button>
</Link>
</div>
}
function Error() {
console.log("Attempting to render ErrorMenu");
return <h1>Error: URL not found</h1>
}
function PopupApp() {
return <Router>
<Routes>
<Route path={PATH_ROOT} element={<MainMenu />} />
<Route path={PATH_OPTIONS} element={<OptionsMenu />} />
<Route path="*" element={<Error />} />
</Routes>
</Router>
}
export default PopupApp;
EDIT: Since a minimum reproducible example was requested, I've prepared one below. Instructions for setting up the React project are listed after the code snippet.
popup.html
<!DOCTYPE html>
<html>
<head>
<title>Extension Sandbox</title>
</head>
<body>
<div id="popup-root"></div>
</body>
</html>
popup.js
import React from "react";
import { createRoot } from "react-dom/client";
import PopupApp from "./PopupApp.jsx";
const popup = createRoot(document.getElementById("popup-root"));
popup.render(
<React.StrictMode>
<PopupApp />
</React.StrictMode>
);
PopupApp.jsx
import React from "react";
import ReactDOM from "react-dom"
import { StaticRouter as Router, Routes, Route, Link } from "react-router";
/* ********* *
* CONSTANTS *
*************/
const PATH_ROOT = "/";
const PATH_OPTIONS = "/options"
/* **************** *
* REACT COMPONENTS *
********************/
function MainMenu() {
return <div className="mainMenu">
<h1>Main</h1>
<Link to={PATH_OPTIONS}>
<button>Options</button>
</Link>
</div>
}
function OptionsMenu() {
return <div>
<h1>Options</h1>
<Link to={PATH_ROOT}>
<button>Back</button>
</Link>
</div>
}
function Error() {
console.log("Attempting to render ErrorMenu");
return <h1>Error: URL not found</h1>
}
function PopupApp() {
return <Router>
<Routes>
<Route path={PATH_ROOT} element={<MainMenu />} />
<Route path={PATH_OPTIONS} element={<OptionsMenu />} />
<Route path="*" element={<Error />} />
</Routes>
</Router>
}
export default PopupApp;
webpack.config.js
const path = require("path");
const HTMLWebpackPlugin = require("html-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
module.exports = {
entry: "./popup.js",
output: {
clean: true,
filename: "bundle.js",
path: path.resolve(__dirname, "dist")
},
module: {
rules: [
{
test: /.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: [
"@babel/preset-env",
["@babel/preset-react", {"runtime": "automatic"}]
]
}
}
}
]
},
plugins: [
new HTMLWebpackPlugin({
title: "Extension",
filename: "./popup.html",
template: "./popup.html"
}),
new CopyWebpackPlugin({
patterns: [
{ from: "manifest.json", to: "manifest.json" }
]
})
]
}
manifest.json
{
"manifest_version": 3,
"name": "React Router",
"version": "1.0",
"action": {
"default_popup": "popup.html"
}
}
Installation Instructions
npm init -ynpm install react, npm install react-dom, and npm install react-routernpm install -D webpacknpm install -D @babel/core, npm install -D babel-loader, npm install -D @babel/preset-react, and npm-install -D @babel/preset-envnpm install -D html-webpack-plugin and npm install -D copy-webpack-pluginpackage.json's scripts object to include the property "build": "webpack --mode production".npm run build to build the project. Webpack will generate a dist folder containing the build.about:debugging in Firefox, click "Load Temporary Add-On," and then click the manifest.json file in the dist directory in the project folder.about:debugging page.Thanks for sharing such a detailed MRE!
I think the issue might be in using StaticRouter. Try and use MemoryRouter instead as it is the standard for extensions.
import { MemoryRouter as Router, Routes, Route, Link } from "react-router-dom";
Update:
The reason why MemoryRouter works and StaticRouter doesn't is rooted in they behave under the hood.
StaticRouter is used for server-side rendering scenarios when the user isn’t actually clicking around, so the location never actually changes (source: Documentation). It doesn't create or manage history. Basically, StaticRouter just renders a snapshot of the UI for a given path. No transitions. It is used for static rendering. What happens when you use Link with StaticRouter:
<Link to="/options">
↓
navigate("/options")
↓
StaticRouter: "Sorry, I don't know how to change location"
↓
React Router warns:
"You should call navigate() in a useEffect()..."
↓
No re-render → still stuck at the old page
On the other hand, MemoryRouter is similar to the more popularly used BrowserRouter or HashRouter. The difference being that the state of the location is stored in memory (not the URL). Very similar to using React states and conditional rendering.