Chrome extension using Vite & React, Build and Deployment
Tags: extensions, javascript, react, vite
Published: 2023-11-27 13:02:21

Hi, It may seem that the internet is full of guides, however, I haven't found any comprehensive, one that could cover all the steps of building extension from a project setup to deployment. So in this post, I will try to fix this this enormous problem and cover all these steps. Feel free to navigate to the needed one if you already pass through some, using the navigation links below: 

- [Environment setup](#environment-setup)
- [Implementing feature](#implementing-feature)
- [Deployment](#deployment)

---

## Environment setup

First of all, let's setup the project using [Vite](https://vitejs.dev/) with React and install some dependencies. 

```bash
npm create vite@latest chrome-extension -- --template react-swc-ts
```
```bash
npm i
```
```bash
npm run dev
```
As a result, the app is running on some port, in my case it is 5173. That is not the common 3000 port, it may seem as nothing but for me I caused lots of troubles on the deployment stage, so lets change this to common 3000 port. It can be done inside `vite.config.ts` file by defining server properly and assigning port to 3000: 

```typescript
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  server: {
    port: 3000,
  },
});
```
Now restart the app, it will be running on the 3000 port.

For styling, I prefer using [tailwindcss](https://tailwindcss.com/) so let's install this: 
```bash
npm install -D tailwindcss postcss autoprefixer
```
```bash
npx tailwindcss init -p
```
And add the Tailwind directives to `index.css`:

```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

Alos for merging classes I will install two more libs: 
```bash
npm install -D tailwind-merge clsx
```
And create `utils.ts` with `cn` function that will help merging classes: 

```typescript
import { type ClassValue, clsx } from "clsx";;
import { twMerge } from "tailwind-merge";

export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
```

**_Manifest_**
The base setup is over. Now for working inside the browser as an extension app needs the [manifest](https://crxjs.dev/vite-plugin) file, which is the important part as the manifest defines the scripts, pages, and permission that the app requires for working.
For these, we need to install CRXJS lib and create `manifest.json` file in the root of directory:

```bash
npm i @crxjs/vite-plugin@beta -D
```

```typescript
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import { crx } from "@crxjs/vite-plugin";
import manifest from "./manifest.json";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), crx({ manifest })],
  server: {
    port: 3000,
  },
});
```

```json
{
  "manifest_version": 3,
  "name": "Chrome extension",
  "version": "1.0.0",
  "action": { "default_popup": "index.html" },
  "icons": {
    "16": "icon_logo_16px.png",
    "32": "icon_logo_32px.png",
    "48": "icon_logo_48px.png",
    "128": "icon_logo_128px.png"
  }
}
```
_The icons are not required they just improve experience and should stored in public folder._

Restart the app and open the chrome -> chrome://extensions/. Enable the developer mode, click **_load unpacked_** and select the **_dist_** folder inside your repository. In a result your extension will be available for testing and development and if you active it you will get something like this: 
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qo176hsceu9kmlb6b8jo.png)


## Implementing feature
Now that the setup is over we need to build the app, however, make sure that if you build some custom extension your manifest fulfils all needs requirement (for cases if the extension directly interacts with a current tab, collect some data, etc.). I will build just a simple calculator so my manifest does not require any addition. I will not explain the logic behind the app and jus share the code for it: 

```tsx
import { Calculator } from "./components/calculator";

function App() {
  return (
    <div className="container w-[25rem] p-1 bg-zinc-800">
      <Calculator />
    </div>
  );
}

export default App;
```
```tsx
import { cn } from "../utils";

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {}

export const Button: React.FC<ButtonProps> = ({ className, ...props }) => {
  return (
    <button
      className={cn(
        "text-white px-2 py-4 text-2xl rounded-md bg-zinc-300/10 hover:bg-zinc-300/40 transition-colors duration-300",
        className
      )}
      {...props}
    />
  );
};
```
```tsx
interface DisplayProps {
  value: string;
}

export const Display: React.FC<DisplayProps> = ({ value }) => {
  return (
    <div className="text-right text-4xl p-2 pr-3 text-white w-full bg-transparant rounded-md">
      {value}
    </div>
  );
};
```
```tsx
import { useReducer } from "react";
import { Display } from "./display";
import { Button } from "./buttons";

interface CalculatorState {
  value: number | null;
  displayValue: string;
  operator: string | null;
  waitingForOperand: boolean;
}

type CalculatorAction = {
  type:
    | "inputDigit"
    | "inputDot"
    | "toggleSign"
    | "inputPercent"
    | "performOperation"
    | "clearAll"
    | "clearDisplay"
    | "clearLastChar";
  payload?: any;
};

const CalculatorOperations: {
  [key: string]: (prevValue: number, nextValue: number) => number;
} = {
  "/": (prevValue, nextValue) => prevValue / nextValue,
  "*": (prevValue, nextValue) => prevValue * nextValue,
  "+": (prevValue, nextValue) => prevValue + nextValue,
  "-": (prevValue, nextValue) => prevValue - nextValue,
  "=": (nextValue) => nextValue,
};

const initState: CalculatorState = {
  value: null,
  displayValue: "0",
  operator: null,
  waitingForOperand: false,
};

const reducer = (state: CalculatorState, action: CalculatorAction) => {
  switch (action.type) {
    case "inputDigit": {
      console.log(action);

      if (state.waitingForOperand) {
        return {
          ...state,
          displayValue: String(action.payload),
          waitingForOperand: false,
        };
      }
      console.log(state);

      return {
        ...state,
        displayValue:
          state.displayValue === "0"
            ? String(action.payload)
            : state.displayValue + action.payload,
      };
    }
    case "inputDot": {
      const { displayValue } = state;

      if (!/\./.test(displayValue)) {
        return {
          ...state,
          displayValue: displayValue + ".",
          waitingForOperand: false,
        };
      }

      return state;
    }
    case "toggleSign": {
      const { displayValue } = state;
      const newValue = parseFloat(displayValue) * -1;

      return { ...state, displayValue: String(newValue) };
    }
    case "inputPercent": {
      const { displayValue } = state;
      const currentValue = parseFloat(displayValue);

      if (currentValue === 0) return state;

      const fixedDigits = displayValue.replace(/^-?\d*\.?/, "");
      const newValue = parseFloat(displayValue) / 100;

      return {
        ...state,
        displayValue: String(newValue.toFixed(fixedDigits.length + 2)),
      };
    }
    case "performOperation": {
      const { value, displayValue, operator } = state;
      const inputValue = parseFloat(displayValue);

      const newState = { ...state };

      if (value == null) {
        newState.value = inputValue;
      }

      if (operator) {
        const currentValue = value || 0;
        const newValue = CalculatorOperations[operator](
          currentValue,
          inputValue
        );

        newState.value = newValue;
        newState.displayValue = String(newValue);
      }

      newState.waitingForOperand = true;
      newState.operator = action.payload;

      return newState;
    }
    case "clearAll": {
      return {
        value: null,
        displayValue: "0",
        operator: null,
        waitingForOperand: false,
      };
    }
    case "clearDisplay": {
      return {
        ...state,
        displayValue: "0",
      };
    }
    case "clearLastChar": {
      const { displayValue } = state;

      return {
        ...state,
        displayValue: displayValue.substring(0, displayValue.length - 1) || "0",
      };
    }
    default:
      return state;
  }
};

export const Calculator: React.FC = () => {
  const [state, dispatch] = useReducer(reducer, initState);

  const { displayValue } = state;
  const clearDisplay = displayValue !== "0";
  const clearText = clearDisplay ? "C" : "AC";

  return (
    <>
      <div className="mb-1">
      <Display value={displayValue} />
      </div>
      <div className="grid grid-cols-4 gap-[0.2rem]">
        <Button
          onClick={() =>
            clearDisplay
              ? dispatch({ type: "clearDisplay" })
              : dispatch({ type: "clearAll" })
          }
        >
          {clearText}
        </Button>
        <Button onClick={() => dispatch({ type: "toggleSign" })}>±</Button>
        <Button onClick={() => dispatch({ type: "inputPercent" })}>%</Button>
        <Button
          onClick={() => dispatch({ type: "performOperation", payload: "/" })}
        >
          ÷
        </Button>
        <Button onClick={() => dispatch({ type: "inputDigit", payload: 7 })}>
          7
        </Button>
        <Button onClick={() => dispatch({ type: "inputDigit", payload: 8 })}>
          8
        </Button>
        <Button onClick={() => dispatch({ type: "inputDigit", payload: 9 })}>
          9
        </Button>
        <Button
          onClick={() => dispatch({ type: "performOperation", payload: "*" })}
        >
          ×
        </Button>
        <Button onClick={() => dispatch({ type: "inputDigit", payload: 4 })}>
          4
        </Button>
        <Button onClick={() => dispatch({ type: "inputDigit", payload: 5 })}>
          5
        </Button>
        <Button onClick={() => dispatch({ type: "inputDigit", payload: 6 })}>
          6
        </Button>
        <Button
          onClick={() => dispatch({ type: "performOperation", payload: "-" })}
        >
          −
        </Button>
        <Button onClick={() => dispatch({ type: "inputDigit", payload: 1 })}>
          1
        </Button>
        <Button onClick={() => dispatch({ type: "inputDigit", payload: 2 })}>
          2
        </Button>
        <Button onClick={() => dispatch({ type: "inputDigit", payload: 3 })}>
          3
        </Button>
        <Button
          onClick={() => dispatch({ type: "performOperation", payload: "+" })}
        >
          +
        </Button>
        <Button
          onClick={() => dispatch({ type: "inputDigit", payload: 0 })}
          className="col-span-2"
        >
          0
        </Button>
        <Button
          onClick={() =>
            dispatch({
              type: "inputDot",
            })
          }
        >
          ●
        </Button>
        <Button
          onClick={() => dispatch({ type: "performOperation", payload: "=" })}
        >
          =
        </Button>
      </div>
    </>
  );
};
```

## Deployment
To be able to deploy extension you need to register in [Chrome web store](https://chromewebstore.google.com/)  as a developer, which requires payment, however, then you will be able to deploy as many extensions as you want. Now need to build an app and compress the dist folder to ZIP: 
```bash
npm run build
```
Click _**new item**_ in the dashboard and upload the compressed build, after that, you need to fill in all necessary info regarding your extension: 
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ftkzxavx2kptsijce30j.png)
The **_Save draft_** button will help you find missing fields and other errors. When all are filed and complete you will be able to submit an extension for a review, it may take from a few minutes to a few days depending on what you app supposed to do (for me it took several hours). 

After the extension is reviewed, it will be published automatically, you can find it in store using its id:  

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ttxbe95mn0phk6ybup5c.png)

Now the extension is publicly available for everyone, so enjoy it.

## Conclusion
I do not know what needs to be sad in the end... I hope I covered all the steps to build and deploy a simple extension using React and Vite. Below this, I post links to the repo and extension in the store maybe they will be useful for someone)) 

[GitHub repository link](https://github.com/cookieMonsterDev/calculator-chrome-extension)

[Extension link](https://chromewebstore.google.com/detail/calculator/fonimcghnndmejinmgpoabakpmpeojjk)