Building Chrome extension with Vite ⚡️
Tags: tutorial, typescript, vite
Published: 2022-01-06 14:43:04

## Introduction

Browser extensions are gaining more and more popularity recently. They're not a simple mini web applications any more, in most cases they are well tailored, profitable products used by hundreds of users every day. And once they grow in size, it's worth to consider building them using some helpful JavaScript libraries.

In this article, we will create a simple Chrome extension, that will be responsible for displaying recent top posts from [DEV Community](https://dev.to/). For this, we will use [Preact](https://preactjs.com/) bootstrapped with [Vite](https://vitejs.dev/) build tool.

Here's a little sneak peek 👀

![Screenshot of extension built in this article presenting list of links to top articles from DEV Community](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/etlqlq6lp9qhur8j10ps.png)

## Stack

Before we start, let's talk about the tech-stack that we will use.

### Vite

If you're not familiar with it already, [Vite](https://vitejs.dev/) is a fast and simple build tool for the web. Basically it makes things easier if it's about starting a new project, it's superfast and offers a lot of pre-defined templates, so you don't have to worry about configuring webpack, transpiling SCSS to CSS etc.

### Preact

Preact is the JavaScript library, as the [docs](https://preactjs.com/guide/v10/getting-started) states it's:

> Fast 3kB alternative to React with the same modern API

Of course, there are some [differences](https://preactjs.com/guide/v10/differences-to-react) between these two libraries, but they are not that crucial and if you're familiar with React you should quickly figure out how Preact works.

## Code

First we need to initialize our project with Vite, we can do this by running the following command 👇

```
yarn create vite dev-articles-extension --template preact-ts
```

As you can see, our project name is `dev-articles-extension` and we used `preact-ts` preset since we want to use Preact with TypeScript. Running this command will create a directory with all necessary files to start working on a front-end application.

Now let's navigate to our project, install required dependencies, run code in development mode, navigate to [`http://localhost:3000/`](http://localhost:3000/) and enjoy the magic 🪄

```
cd dev-articles-extension && yarn && yarn dev
```

Time for some code. We need to fetch recent top posts from [DEV API](https://developers.forem.com/api) and display them in a list, also we need to handle loading and error states, so let's do it. Replace `app.tsx` file with the following code 👇

```tsx
import { useEffect, useState } from "preact/hooks";

type Article = {
  id: string;
  title: string;
  url: string;
  positive_reactions_count: number;
  published_timestamp: string;
  reading_time_minutes: number;
};

const useArticles = () => {
  const [articles, setArticles] = useState<Article[]>([]);
  const [error, setError] = useState("");
  const [isLoading, setLoading] = useState(true);

  useEffect(() => {
    const fetchArticles = async () => {
      try {
        const response = await fetch("https://dev.to/api/articles?top=1");

        if (!response.ok) {
          throw new Error("Response is not ok");
        }

        const data = await response.json();
        setArticles(data);
      } catch (error) {
        setError("An error ocurred while fetching articles");
      } finally {
        setLoading(false);
      }
    };

    fetchArticles();
  }, []);

  return { articles, error, isLoading };
};

export const App = () => {
  const { articles, error, isLoading } = useArticles();

  return (
    <div className="container">
      {isLoading ? (
        <div className="spinner">
          <span className="spinner__circle" />
          <span>Please wait...</span>
        </div>
      ) : error ? (
        <span className="error">{error}</span>
      ) : (
        <>
          <h1 className="title">Top posts on DEV Community</h1>
          <ul className="articles">
            {articles.map(
              ({
                id,
                title,
                url,
                positive_reactions_count,
                published_timestamp,
                reading_time_minutes,
              }) => (
                <li key={id} className="article">
                  <a
                    href={url}
                    target="_blank"
                    rel="noreferrer"
                    className="article__link"
                  >
                    {title}
                  </a>
                  <ul className="article__details">
                    {[
                      {
                        title: "Published at",
                        icon: "🗓",
                        label: "Calendar emoji",
                        value: new Date(
                          published_timestamp
                        ).toLocaleDateString(),
                      },
                      {
                        title: "Reading time",
                        icon: "🕑",
                        label: "Clock emoji",
                        value: `${reading_time_minutes} min`,
                      },
                      {
                        title: "Reactions count",
                        icon: "❤️ 🦄 🔖",
                        label: "Heart, unicorn and bookmark emojis",
                        value: positive_reactions_count,
                      },
                    ].map(({ title, icon, label, value }, index) => (
                      <li
                        key={`${id}-detail-${index}`}
                        className="article__detail"
                        title={title}
                      >
                        <span role="img" aria-label={label}>
                          {icon}
                        </span>
                        <span>{value}</span>
                      </li>
                    ))}
                  </ul>
                </li>
              )
            )}
          </ul>
        </>
      )}
    </div>
  );
};
```

This code is pretty self-explanatory, but if any part of it is unclear to you, let me know in the comments.

Application logic is ready, now it's time for some styling. Nothing crazy, just replace `index.css` file with this content 👇

```css
html {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Open Sans", "Helvetica Neue", sans-serif;
  font-size: 14px;
}

body {
  font-size: 1rem;
  color: #0f172a;
  margin: 0;
}

.container {
  min-width: 30em;
  padding: 1em;
  box-sizing: border-box;
}

.spinner {
  display: flex;
  align-items: center;
}

.spinner__circle {
  display: block;
  width: 1.25em;
  height: 1.25em;
  border: 3px solid #bfdbfe;
  border-top-color: #2563eb;
  border-radius: 50%;
  box-sizing: border-box;
  margin-right: 0.5em;
  animation: spin 1s ease infinite;
}

.error {
  display: block;
  padding: 1em;
  box-sizing: border-box;
  border-radius: 10px;
  background-color: #ffe4e6;
  color: #e11d48;
}

.title {
  font-size: 1.75rem;
  margin: 0 0 1rem;
}

.articles {
  list-style-type: none;
  padding: 0;
  margin: 0;
}

.article:not(:last-child) {
  margin-bottom: 1em;
}

.article__link {
  display: block;
  margin-bottom: 0.15em;
  color: #2563eb;
  text-decoration: none;
}
.article__link:hover {
  text-decoration: underline;
}

.article__details {
  display: flex;
  align-items: center;
  list-style-type: none;
  margin: 0;
  padding: 0;
  font-size: 0.8em;
  color: #64748b;
}

.article__detail:not(:last-child) {
  margin-right: 0.5rem;
}
.article__detail span[role="img"] {
  margin-right: 0.25rem;
}

@media (prefers-reduced-motion) {
  .spinner__circle {
    animation: spin 2s ease infinite;
  }
}

@keyframes spin {
  from {
    transform: rotate(0);
  }
  to {
    transform: rotate(360deg);
  }
}
```

At this point, you should have a fully functional application, but weren't we supposed to build a Chrome extension? We are one step away from that. Let's create a [manifest](https://developer.chrome.com/docs/extensions/mv3/manifest/) file that will provide important information about our extension to the Chrome browser.

```
touch src/manifest.json
```

And fill it with required values 👇

```json
{
  "manifest_version": 3,
  "name": "DEV Articles",
  "description": "A quick way to browse top posts from DEV Community.",
  "version": "0.0.1",
  "action": {
    "default_popup": "index.html"
  }
}
```

Now we're ready to build our extension.

## Building and installing

Vite provides us with `build` command that creates `dist/` directory with all compiled files, but we also need to remember about copying `src/manifest.json` file there. In order to avoid doing this "by hand" every time we build our project, we will add `build-extension` script to the `package.json` that will do it automatically for us.

```json
"scripts": {
  "build-extension": "yarn build && cp src/manifest.json dist/"
}
```

Once we added this, let's run it.

```
yarn build-extension
```

After running this command, you should see `dist/` directory with `manifest.json` file in it. Now, let's navigate to [`chrome://extensions`](chrome://extensions) panel and upload `dist/` directory there like so 👇

![Screenshot from Chrome extensions panel with instruction how to upload extension](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xwflfibjpp9vog0gfuz0.png)

Viola, that's it! Your extension is ready to use.

## Repository

I didn't prepare any live demo of this extension, but if you want to take a look at the full code, check out this repository on GitHub. Feel free to contribute.

{% github rago4/dev-articles-extension %}

Thanks for reading! 👋