Real Vite-React HMR in Chrome Extension Content Scripts
Tags: react, webdev, vite, tutorial
Published: 2022-03-18 20:26:00

> A content script is JavaScript from a Chrome Extension that the browser executes on a designated host page. It shares the DOM with the host page but has a separate JavaScript environment.

If you've done any work on Chrome Extensions, you know that content scripts are one of the essential tools at your disposal. However, you also know they are a PITA to develop. 

The traditional content script developer experience is as follows:

1. Make changes to your code
2. Reload the extension in `chrome://extensions`
3. Reload the host page
4. Check that things work right
5. Repeat

Forget a step, and your changes don't show up. Hopefully, you realize what's happening before you start debugging. 😅

Adding DOM elements to a website is a standard content script use case. Unfortunately, you need to bundle your code to use a framework like React or Vue in a content script.

Vite does a great job serving code for the browser, but content scripts load their code from the file system, so Vite's sweet HMR experience doesn't work out of the box. Until now.

## Good News for Content Script DX

You can have HMR in content scripts; you can say goodbye to the tedious workflow they represent. My name is Jack Steam, and I'm the creator of the CRXJS Vite plugin. Today CRXJS brings an authentic Vite HMR experience to content scripts for the first time. Let me show you how to get started.

If you're coming from my first article, [Create a Vite-React Chrome Extension in 90 seconds](https://dev.to/jacksteamdev/create-a-vite-react-chrome-extension-in-90-seconds-3df7), you already know how to get started; you can skip this next bit. Instead, scroll down to the next heading, ["Add a content script"](#add-a-content-script).

## Getting Started

Using your favorite package manager, initialize a new Vite project. Follow the prompts to set up your project. CRXJS works with React and Vue, but we'll use React for this guide.

> CRXJS doesn't yet work with Vite 3, but support is coming soon.

```sh
npm init vite@^2
cd <your-project-name>
npm install
npm i @crxjs/vite-plugin@latest -D
```

Open `vite.config.js` and add CRXJS:

```javascript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
+ import { crx } from '@crxjs/vite-plugin'

export default defineConfig({
- plugins: [react()]
+ plugins: [react(), crx()]
})
```

Chrome Extensions declare their resources using a `manifest.json` file. Create your manifest next to `vite.config.js` with these fields:

```json
{
  "manifest_version": 3,
  "name": "Vite React Chrome Extension",
  "version": "1.0.0"
}
```

Go back to `vite.config.js` and add the manifest:

```javascript
// other imports...
+ import manifest from './manifest.json'

export default defineConfig({
- plugins: [react(), crx()]
+ plugins: [react(), crx({ manifest })]
})
```

## Add a content script

We declare content scripts with a list of JavaScript files and match patterns for the pages where Chrome should execute our content script. In `manifest.json`, create the field `content_scripts` with an array of objects:

```jsonc
{
  // other fields...
  "content_scripts": [{
    "js": ["src/main.jsx"],
    "matches": ["https://www.google.com/*"]
  }]
}
```

Here we're telling Chrome to execute `src/main.jsx` on all pages that start with `https://www.google.com`.

## Create the root element

Content scripts don't use an HTML file, so we need to create our root element and append it to the DOM before mounting our React app. Open `src/main.jsx` and add a root element.

```jsx
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'

+ const root = document.createElement('div')
+ root.id = 'crx-root'
+ document.body.append(root)

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
- document.getElementById('root')
+ root
)
```

## Get the right URL

Content scripts share the origin of their host page. We need to get a URL with our extension id for static assets like images. Let's go to `src/App.jsx` and do that now.

```jsx
<img
- src={logo}
+ src={chrome.runtime.getURL(logo)}
  className="App-logo"
  alt="logo"
/>
```

Now our content script is ready for action! But, first, let's load our extension in Chrome.

## Load the extension

Start Vite in the terminal.

```sh
npm run dev
```

Open the extensions dashboard at the URL `chrome://extensions` and turn on development mode using the switch in the top right corner. Next, load your extension by dragging the `dist` folder onto the extensions dashboard.

## Profit with HMR

Navigate to `https://www.google.com` and scroll to the bottom of the page. There's our familiar Vite Hello World!

Notice how the counter button doesn't look like a button. That's because Google's styles affect our content script elements. The same goes the other way: our styles change Google's styles. 

Let's fix that. Replace everything in `src/index.css` with this:

```css
#crx-root {
  position: fixed;
  top: 3rem;
  left: 50%;
  transform: translate(-50%, 0);
}

#crx-root button {
  background-color: rgb(239, 239, 239);
  border-color: rgb(118, 118, 118);
  border-image: initial;
  border-style: outset;
  border-width: 2px;
  margin: 0;
  padding: 1px 6px;
}
```

CRXJS will quickly rebuild the content script, and our CSS changes will take effect. Now our `div` position is fixed, and that button looks more like a button! Click the count button and play around with `src/App.jsx` to see Vite HMR at work.

We need your feedback! If something doesn't work for you, please [create an issue](https://github.com/crxjs/rollup-plugin-chrome-extension/issues). 

Conversely, if CRXJS has improved your developer experience, please consider [sponsoring me on GitHub](https://github.com/sponsors/jacksteamdev) or [give me a shoutout on Twitter](https://twitter.com/JackSteamDev). See you next time.

Good luck building your Chrome Extension!