Using React for Browser Extensions
Cover photo by Florian Olivo on Unsplash
So you want to build a browser extension with React. I get it. You’ve got a pile of state and a UI that needs to update whenever a user clicks an icon. It makes sense to reach for the tool you use every day.
The naive approach
I used to think I could just drop a root React component into my content script. I did this:
const root = document.createElement('div');document.body.appendChild(root);createRoot(root).render(<MyExtensionApp />);It works, mostly. Until it doesn’t. You’ll run into conflicts with the host page CSS immediately. If the website uses a global reset, your extension will look like a mess. Your styles will leak, and their styles will break your buttons.
Isolating your React app
To fix this, you need a Shadow DOM. This creates a boundary so your styles don’t bleed out and the host page styles don’t bleed in. It’s not perfect because some global fonts might still leak, but it’s the standard for a reason.
const host = document.createElement('div');const shadow = host.attachShadow({ mode: 'open' });document.body.appendChild(host);
// Inject styles into the shadow rootconst style = document.createElement('style');style.textContent = `...your css here...`;shadow.appendChild(style);
const root = document.createElement('div');shadow.appendChild(root);createRoot(root).render(<MyExtensionApp />);Now your React app lives in its own little world. (It’s still messy, but at least it’s a contained mess.)
But wait, state management?
You might think, “Why not use Redux for everything?” I hear you. “Redux is overkill for a tiny extension.” Honestly, you’re right. You don’t need a heavy state library. For most extensions, React state combined with the chrome.storage API is plenty.
I usually write a custom hook to sync state between the background script and the popup. It looks something like this:
function useStorage(key) { const [value, setValue] = useState(null);
useEffect(() => { chrome.storage.local.get(key, (res) => setValue(res[key])); }, [key]);
return [value, setValue];}Performance caveats
I should mention that React adds overhead. Extensions run in a constrained environment where every millisecond matters. If your extension fires on every single page load, a full React bundle might be heavy.
Sometimes, vanilla JavaScript is better. I know, that sounds like a regression. If all you are doing is toggling a few classes or reading the DOM, don’t ship a library. Use a library when the UI complexity demands it, not because you’re uncomfortable writing raw DOM nodes.
Are you worried about bundle sizes? You should be. Use a bundler like Vite to minimize your code. Don’t include huge dependencies just to parse a date or format a string. Keep it lean.
What about Web Components? They pair nicely with React if you want to expose parts of your extension to other scripts, but that’s a rabbit hole for another day. Have you tried building an extension with a different framework lately?