Skip to main content
CRXJS works seamlessly with React, providing hot module replacement (HMR) and all the modern development features you expect from Vite.

Installation

Install the required dependencies:
npm install react react-dom
npm install -D @vitejs/plugin-react @types/react @types/react-dom

Vite Configuration

Configure Vite to use both the React and CRXJS plugins:
vite.config.ts
import path from 'node:path'
import { crx } from '@crxjs/vite-plugin'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
import manifest from './manifest.config'

export default defineConfig({
  resolve: {
    alias: {
      '@': `${path.resolve(__dirname, 'src')}`,
    },
  },
  plugins: [
    react(),
    crx({ manifest }),
  ],
  server: {
    cors: {
      origin: [
        /chrome-extension:\/\//,
      ],
    },
  },
})
The react() plugin must be placed before crx() in the plugins array.

TypeScript Configuration

Set up TypeScript for React with proper types:
tsconfig.app.json
{
  "compilerOptions": {
    "target": "ES2020",
    "jsx": "react-jsx",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    },
    "types": ["vite/client", "chrome"],
    "strict": true,
    "noEmit": true,
    "isolatedModules": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}
Create a React popup component:
src/popup/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './index.css'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)
src/popup/App.tsx
import crxLogo from '@/assets/crx.svg'
import reactLogo from '@/assets/react.svg'
import viteLogo from '@/assets/vite.svg'
import HelloWorld from '@/components/HelloWorld'
import './App.css'

export default function App() {
  return (
    <div>
      <a href="https://vite.dev" target="_blank" rel="noreferrer">
        <img src={viteLogo} className="logo" alt="Vite logo" />
      </a>
      <a href="https://reactjs.org/" target="_blank" rel="noreferrer">
        <img src={reactLogo} className="logo react" alt="React logo" />
      </a>
      <a href="https://crxjs.dev/vite-plugin" target="_blank" rel="noreferrer">
        <img src={crxLogo} className="logo crx" alt="crx logo" />
      </a>
      <HelloWorld msg="Vite + React + CRXJS" />
    </div>
  )
}

Content Script with React

Inject React into a webpage using a content script:
src/content/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './views/App.tsx'

console.log('[CRXJS] Hello world from content script!')

const container = document.createElement('div')
container.id = 'crxjs-app'
document.body.appendChild(container)
createRoot(container).render(
  <StrictMode>
    <App />
  </StrictMode>,
)
Update your manifest to include the content script:
manifest.config.ts
import { defineManifest } from '@crxjs/vite-plugin'

export default defineManifest({
  manifest_version: 3,
  name: 'My React Extension',
  version: '1.0.0',
  action: {
    default_popup: 'src/popup/index.html',
  },
  content_scripts: [{
    js: ['src/content/main.tsx'],
    matches: ['https://*/*'],
  }],
})

Hot Module Replacement

CRXJS provides full HMR support for React:
  • Component changes update instantly without losing state
  • CSS changes apply immediately
  • Manifest changes automatically reload the extension
React Fast Refresh works out of the box with CRXJS. Make changes to your components and see them update in real-time.

Best Practices

Use React DevTools

Install the React DevTools extension to inspect your components. Both extensions can run simultaneously.

Code Splitting

Leverage React.lazy() for code splitting:
import { lazy, Suspense } from 'react'

const Settings = lazy(() => import('./components/Settings'))

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Settings />
    </Suspense>
  )
}

State Management

Use Chrome’s storage APIs with React hooks:
import { useEffect, useState } from 'react'

function useStorage(key: string, defaultValue: any) {
  const [value, setValue] = useState(defaultValue)

  useEffect(() => {
    chrome.storage.sync.get([key], (result) => {
      setValue(result[key] ?? defaultValue)
    })
  }, [key])

  const updateValue = (newValue: any) => {
    chrome.storage.sync.set({ [key]: newValue })
    setValue(newValue)
  }

  return [value, updateValue]
}

Package.json Scripts

package.json
{
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  },
  "devDependencies": {
    "@crxjs/vite-plugin": "latest",
    "@types/chrome": "^0.0.313",
    "@types/react": "^19.0.0",
    "@types/react-dom": "^19.0.0",
    "@vitejs/plugin-react": "^4.3.0",
    "typescript": "~5.7.0",
    "vite": "^6.0.0"
  }
}

Next Steps

Build docs developers (and LLMs) love