# React Integration with WXT Complete guide for building Chrome extensions with React and WXT. ## Setup ### Initialize with React Template ```bash npm create wxt@latest -- --template react-ts cd my-extension npm install ``` ### Manual Setup ```bash npm install react react-dom npm install -D @types/react @types/react-dom @wxt-dev/module-react ``` **wxt.config.ts:** ```typescript import { defineConfig } from 'wxt'; export default defineConfig({ modules: ['@wxt-dev/module-react'], manifest: { content_security_policy: { extension_pages: "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'", }, }, }); ``` ## Entry Point Patterns ### Popup with React **Directory structure:** ``` entrypoints/popup/ ├── index.html ├── main.tsx # Entry point ├── App.tsx # Root component └── components/ # UI components ├── Header.tsx └── Settings.tsx ``` **entrypoints/popup/index.html:** ```html Extension Popup
``` **entrypoints/popup/main.tsx:** ```typescript import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import './style.css'; ReactDOM.createRoot(document.getElementById('root')!).render( ); ``` **entrypoints/popup/App.tsx:** ```typescript import { useState, useEffect } from 'react'; export default function App() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { loadData(); }, []); async function loadData() { try { // Get current tab const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); // Load from storage const result = await browser.storage.local.get('settings'); setData(result.settings); } catch (error) { console.error('Error loading data:', error); } finally { setLoading(false); } } async function handleSave(newSettings: any) { await browser.storage.local.set({ settings: newSettings }); setData(newSettings); } if (loading) { return
Loading...
; } return (

My Extension

{/* Your UI here */}
); } ``` ### Options Page with React **entrypoints/options/index.html:** ```html Extension Options
``` **entrypoints/options/App.tsx:** ```typescript import { useState, useEffect } from 'react'; export default function Options() { const [settings, setSettings] = useState({ theme: 'light', notifications: true, apiKey: '', }); useEffect(() => { // Load settings browser.storage.sync.get('settings').then((result) => { if (result.settings) { setSettings(result.settings); } }); }, []); async function handleSave() { await browser.storage.sync.set({ settings }); // Show success notification await browser.notifications.create({ type: 'basic', title: 'Settings Saved', message: 'Your settings have been saved successfully', iconUrl: '/icon/128.png', }); } return (

Extension Settings

); } ``` ### Content Script with React UI **entrypoints/content.ts:** ```typescript import ReactDOM from 'react-dom/client'; import { ContentScriptApp } from './ContentScriptApp'; export default defineContentScript({ matches: ['*://*.example.com/*'], cssInjectionMode: 'ui', async main(ctx) { // Create shadow root UI const ui = await createShadowRootUi(ctx, { name: 'my-extension-overlay', position: 'overlay', anchor: 'body', onMount: (container) => { // Mount React app in shadow DOM const root = ReactDOM.createRoot(container); root.render(); return root; }, onRemove: (root) => { // Cleanup root?.unmount(); }, }); // Mount UI ui.mount(); }, }); ``` **entrypoints/ContentScriptApp.tsx:** ```typescript import { useState } from 'react'; import './content.css'; export function ContentScriptApp() { const [visible, setVisible] = useState(false); return (
{visible && (

Extension Panel

This is injected into the page!

)}
); } ``` ## React Hooks for Extensions ### useStorage Hook ```typescript // hooks/useStorage.ts import { useState, useEffect } from 'react'; export function useStorage(key: string, defaultValue: T) { const [value, setValue] = useState(defaultValue); const [loading, setLoading] = useState(true); useEffect(() => { // Load initial value browser.storage.local.get(key).then((result) => { if (result[key] !== undefined) { setValue(result[key]); } setLoading(false); }); // Listen for changes const listener = (changes: any, area: string) => { if (area === 'local' && changes[key]) { setValue(changes[key].newValue); } }; browser.storage.onChanged.addListener(listener); return () => { browser.storage.onChanged.removeListener(listener); }; }, [key]); const updateValue = async (newValue: T) => { await browser.storage.local.set({ [key]: newValue }); setValue(newValue); }; return [value, updateValue, loading] as const; } // Usage function MyComponent() { const [theme, setTheme, loading] = useStorage('theme', 'light'); if (loading) return
Loading...
; return ( ); } ``` ### useMessage Hook ```typescript // hooks/useMessage.ts import { useEffect, useCallback } from 'react'; type MessageHandler = ( message: T, sender: browser.Runtime.MessageSender ) => any | Promise; export function useMessage( type: string, handler: MessageHandler ) { useEffect(() => { const listener = ( message: any, sender: browser.Runtime.MessageSender, sendResponse: (response?: any) => void ) => { if (message.type === type) { Promise.resolve(handler(message.payload, sender)) .then(sendResponse) .catch((error) => { console.error(`Error handling message ${type}:`, error); sendResponse({ error: error.message }); }); return true; // Keep channel open } }; browser.runtime.onMessage.addListener(listener); return () => { browser.runtime.onMessage.removeListener(listener); }; }, [type, handler]); } // Usage function MyComponent() { useMessage('get-data', async (payload, sender) => { console.log('Message from:', sender.tab?.url); return { data: 'some data' }; }); return
Component listening for messages
; } ``` ### useTabs Hook ```typescript // hooks/useTabs.ts import { useState, useEffect } from 'react'; export function useTabs() { const [tabs, setTabs] = useState([]); useEffect(() => { // Load initial tabs browser.tabs.query({}).then(setTabs); // Listen for tab changes const onCreated = (tab: browser.Tabs.Tab) => { setTabs((prev) => [...prev, tab]); }; const onRemoved = (tabId: number) => { setTabs((prev) => prev.filter((t) => t.id !== tabId)); }; const onUpdated = (tabId: number, changeInfo: any, tab: browser.Tabs.Tab) => { setTabs((prev) => prev.map((t) => (t.id === tabId ? tab : t)) ); }; browser.tabs.onCreated.addListener(onCreated); browser.tabs.onRemoved.addListener(onRemoved); browser.tabs.onUpdated.addListener(onUpdated); return () => { browser.tabs.onCreated.removeListener(onCreated); browser.tabs.onRemoved.removeListener(onRemoved); browser.tabs.onUpdated.removeListener(onUpdated); }; }, []); return tabs; } // Usage function TabList() { const tabs = useTabs(); return (
    {tabs.map((tab) => (
  • {tab.title} - {tab.url}
  • ))}
); } ``` ## State Management ### Context API for Extension State ```typescript // contexts/ExtensionContext.tsx import { createContext, useContext, useEffect, useState } from 'react'; interface ExtensionState { settings: any; user: any; updateSettings: (settings: any) => Promise; } const ExtensionContext = createContext(null); export function ExtensionProvider({ children }: { children: React.ReactNode }) { const [settings, setSettings] = useState({}); const [user, setUser] = useState(null); useEffect(() => { // Load initial state browser.storage.local.get(['settings', 'user']).then((result) => { setSettings(result.settings || {}); setUser(result.user || null); }); // Listen for storage changes const listener = (changes: any) => { if (changes.settings) { setSettings(changes.settings.newValue); } if (changes.user) { setUser(changes.user.newValue); } }; browser.storage.onChanged.addListener(listener); return () => { browser.storage.onChanged.removeListener(listener); }; }, []); const updateSettings = async (newSettings: any) => { await browser.storage.local.set({ settings: newSettings }); setSettings(newSettings); }; return ( {children} ); } export function useExtension() { const context = useContext(ExtensionContext); if (!context) { throw new Error('useExtension must be used within ExtensionProvider'); } return context; } // Usage in App function App() { return ( ); } ``` ### Zustand for Extension State ```typescript // store/useStore.ts import { create } from 'zustand'; import { persist } from 'zustand/middleware'; interface ExtensionStore { theme: 'light' | 'dark'; notifications: boolean; apiKey: string; setTheme: (theme: 'light' | 'dark') => void; setNotifications: (enabled: boolean) => void; setApiKey: (key: string) => void; } export const useStore = create()( persist( (set) => ({ theme: 'light', notifications: true, apiKey: '', setTheme: (theme) => set({ theme }), setNotifications: (notifications) => set({ notifications }), setApiKey: (apiKey) => set({ apiKey }), }), { name: 'extension-storage', getStorage: () => ({ getItem: async (name) => { const result = await browser.storage.local.get(name); return result[name] || null; }, setItem: async (name, value) => { await browser.storage.local.set({ [name]: value }); }, removeItem: async (name) => { await browser.storage.local.remove(name); }, }), } ) ); // Usage function SettingsComponent() { const { theme, setTheme } = useStore(); return ( ); } ``` ## Styling ### Popular UI Libraries (2025) Modern Chrome extensions commonly use these UI libraries with React: #### shadcn/ui Most popular choice for Chrome extensions in 2025. ```bash npx shadcn@latest init npx shadcn@latest add button card dialog ``` **Benefits:** - Tailwind CSS-based components - Full customization and ownership of code - Copy-paste philosophy - components live in your codebase - Excellent TypeScript support - Works seamlessly with WXT **Example Setup:** ```typescript // components/ui/button.tsx (generated by shadcn) import * as React from "react" import { cn } from "@/lib/utils" export interface ButtonProps extends React.ButtonHTMLAttributes {} const Button = React.forwardRef( ({ className, ...props }, ref) => { return ( ); } ``` ### Styled Components ```bash npm install styled-components npm install -D @types/styled-components ``` ```typescript import styled from 'styled-components'; const Button = styled.button` padding: 8px 16px; background: ${(props) => props.theme.primary}; color: white; border: none; border-radius: 4px; &:hover { opacity: 0.8; } `; export function MyComponent() { return ; } ``` ## Performance Optimization ### Code Splitting ```typescript // Lazy load heavy components import { lazy, Suspense } from 'react'; const HeavyComponent = lazy(() => import('./HeavyComponent')); function App() { return ( Loading...}> ); } ``` ### React.memo for Expensive Components ```typescript import { memo } from 'react'; const ExpensiveComponent = memo(({ data }: { data: any }) => { // Expensive rendering logic return
{/* rendered content */}
; }); ``` ### useMemo and useCallback ```typescript import { useMemo, useCallback } from 'react'; function DataTable({ data }: { data: any[] }) { // Memoize expensive calculations const sortedData = useMemo(() => { return data.sort((a, b) => a.value - b.value); }, [data]); // Memoize callbacks const handleClick = useCallback((id: number) => { console.log('Clicked:', id); }, []); return ( {sortedData.map((item) => ( handleClick(item.id)}> ))}
{item.name}
); } ``` ## Common Patterns ### Loading States ```typescript function DataLoader() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { fetchData() .then(setData) .catch(setError) .finally(() => setLoading(false)); }, []); if (loading) return ; if (error) return ; if (!data) return ; return ; } ``` ### Form Handling ```typescript import { useState, FormEvent } from 'react'; function SettingsForm() { const [formData, setFormData] = useState({ name: '', email: '', notifications: true, }); const handleSubmit = async (e: FormEvent) => { e.preventDefault(); try { await browser.storage.sync.set({ settings: formData }); console.log('Settings saved'); } catch (error) { console.error('Failed to save:', error); } }; return (
setFormData({ ...formData, name: e.target.value })} /> setFormData({ ...formData, email: e.target.value })} />
); } ``` ### Modal/Dialog Patterns ```typescript function ConfirmDialog({ isOpen, onConfirm, onCancel }: any) { if (!isOpen) return null; return (
e.stopPropagation()}>

Are you sure?

); } ``` ## Testing React Extensions ### Component Testing with Vitest ```typescript // Button.test.tsx import { render, screen, fireEvent } from '@testing-library/react'; import { describe, it, expect, vi } from 'vitest'; import { Button } from './Button'; describe('Button', () => { it('renders children', () => { render(); expect(screen.getByText('Click me')).toBeInTheDocument(); }); it('calls onClick when clicked', () => { const handleClick = vi.fn(); render(); fireEvent.click(screen.getByText('Click me')); expect(handleClick).toHaveBeenCalledTimes(1); }); }); ``` ## Common Issues & Solutions ### Issue: React DevTools not working **Solution:** Add to manifest: ```typescript manifest: { content_security_policy: { extension_pages: "script-src 'self' 'unsafe-eval'; object-src 'self'", }, } ``` ### Issue: Hot reload breaks React state **Solution:** Use React Fast Refresh properly: ```typescript // In components, ensure proper export export default function MyComponent() { // Component logic } ``` ### Issue: Storage not syncing between components **Solution:** Use storage change listeners: ```typescript useEffect(() => { const listener = (changes: any) => { if (changes.key) { setState(changes.key.newValue); } }; browser.storage.onChanged.addListener(listener); return () => browser.storage.onChanged.removeListener(listener); }, []); ```