1

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; 

HOW TO SET UP A MINIMUM REPRODUCIBLE EXAMPLE

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

  1. Copy and paste these five files into some fresh directory
  2. Navigate to that directory and run npm init -y
  3. Run npm install react, npm install react-dom, and npm install react-router
  4. Run npm install -D webpack
  5. Run npm install -D @babel/core, npm install -D babel-loader, npm install -D @babel/preset-react, and npm-install -D @babel/preset-env
  6. Run npm install -D html-webpack-plugin and npm install -D copy-webpack-plugin
  7. Modify package.json's scripts object to include the property "build": "webpack --mode production".
  8. Finally, run npm run build to build the project. Webpack will generate a dist folder containing the build.
  9. Navigate to about:debugging in Firefox, click "Load Temporary Add-On," and then click the manifest.json file in the dist directory in the project folder.
  10. To reproduce the error, open up the extension, then click the button. Console output for the extension can be read by clicking "Inspect" on the about:debugging page.
8
  • The Link component is rendered and is meant for declarative navigation, e.g. it needs to be clicked before it does anything, whereas navigate is used for imperative navigation, e.g. some code somewhere actively calls it. I don't see anything in your code actively initiating any navigation actions. Do you have some code elsewhere that is attempting to navigate somewhere? Please see minimal reproducible example and try to include a complete example that reproduces the problem you are trying to solve. Please also update the title to more accurately reflect what you are asking for help with. Commented Sep 24 at 16:01
  • @DrewReese Respectfully, I'm asking exactly what I intend to ask. The snippet I posted is the only part of this project that touches React Router, so I don't have any other code to post. When I click the Link, navigation does not occur, and instead I get the warning that I describe above. Commented Sep 24 at 19:22
  • Are you certain the warning is coming from this specific code? Is there any sort of stacktrace or indication where exactly in the app the issue arises? How is your React app being run in the extension? I only ask because I think I recall seeing the same or similar issue if your React components are being called directly as functions instead of rendered by React as JSX. Again, a complete minimal reproducible example that reproduces the issue that readers could run themselves or more complete details surrounding the code would be appreciated. Commented Sep 24 at 19:34
  • I know the warning is coming from this code because the warning arises every time I click the button. The React app is being bundled by Webpack into the file hierarchy needed for browser extensions. The Webpack export is very unlikely the problem, since this pipeline has been working for nearly the lifetime of the project.That said, I can provide a manifest.json file to open this in Firefox as an extension some time tonight. Commented Sep 24 at 19:38
  • In addition to that, can you share where and how PopupApp and/or this React app is rendered or called? Commented Sep 24 at 19:51

1 Answer 1

5
+50

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.

Sign up to request clarification or add additional context in comments.

2 Comments

I stumbled across this a few days ago and it does actually work! I didn't say anything because I figured that I should wait for other answers. That said, I'm still curious as to why this works and StaticRouter doesn't? No worries if you don't know, but I'm really trying to grasp the inner workings.
Sure, updated my answer with an explanation.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.