Chrome Extension React Setup — Developer Guide

5 min read

Setting Up React for Chrome Extension Development

This guide covers building Chrome extensions with React, covering project initialization, architecture patterns, and best practices for modern extension development.

Project Initialization

Create a new Vite project and add the CRXJS plugin:

npm create vite@latest my-extension -- --template react-ts
cd my-extension
npm install vite-plugin-chrome-extension

Configure vite.config.ts:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import crx from 'vite-plugin-chrome-extension';

export default defineConfig({
  plugins: [
    react(),
    crx({ manifest: manifestJson })
  ]
});

Multiple React Roots

Chrome extensions have multiple entry points (popup, options page, side panel). Each should be a separate React app:

src/
├── popup/       # Browser action popup
├── options/     # Extension options page
├── sidepanel/   # Side panel page
└── content/     # Content scripts

Configure multiple HTML entry points in Vite and register them in manifest.json.

Shared Components

Create a common UI library used across all extension contexts:

packages/ui/
├── Button.tsx
├── Input.tsx
└── Modal.tsx

Publish as internal package or use workspace monorepo structure.

State Management

Lightweight, no provider wrapper needed, works across extension contexts:

import { create } from 'zustand';

interface ExtensionStore {
  settings: Settings;
  updateSettings: (settings: Settings) => void;
}

export const useStore = create<ExtensionStore>((set) => ({
  settings: defaultSettings,
  updateSettings: (settings) => set({ settings }),
}));

Redux Toolkit

For complex state needs, Redux Toolkit provides standardized patterns.

Content Scripts with React

Content scripts run in page context. Use shadow DOM for style isolation:

import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

const container = document.createElement('div');
container.id = 'my-extension-root';
document.body.appendChild(container);

const shadow = container.attachShadow({ mode: 'open' });
const reactRoot = document.createElement('div');
shadow.appendChild(reactRoot);

createRoot(reactRoot).render(<App />);

React DevTools

React DevTools works in extension pages. Enable in manifest.json:

{
  "permissions": ["activeTab"]
}

Open DevTools on popup, options, or side panel pages to inspect React component trees.

Routing

Use hash routing for extension pages since they run from chrome-extension://:

import { HashRouter, Routes, Route } from 'react-router-dom';

<HashRouter>
  <Routes>
    <Route path="/popup" element={<Popup />} />
    <Route path="/options" element={<Options />} />
  </Routes>
</HashRouter>

Styling Options

Testing

Custom Hooks for Extension APIs

Create reusable hooks for common extension functionality:

// useStorage.ts
import { useState, useEffect } from 'react';
import { Storage } from 'webextension-polyfill';

export function useStorage<T>(key: string, defaultValue: T) {
  const [value, setValue] = useState<T>(defaultValue);

  useEffect(() => {
    Storage.local.get(key).then((result) => {
      setValue(result[key] ?? defaultValue);
    });
  }, [key]);

  const update = (newValue: T) => {
    Storage.local.set({ [key]: newValue });
    setValue(newValue);
  };

  return [value, update];
}

// useMessage.ts
import { useEffect } from 'react';
import { Runtime } from 'webextension-polyfill';

export function useMessage(callback: (message: any) => void) {
  useEffect(() => {
    const listener = (message: any) => callback(message);
    Runtime.onMessage.addListener(listener);
    return () => Runtime.onMessage.removeListener(listener);
  }, [callback]);
}

Performance Optimization

Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.