Build an Electron app with electron-vite
Tags: electron, vite
Published: 2023-08-30 15:48:30

**Written by [Brian De Sousa](https://blog.logrocket.com/author/briandesousa/)✏️**

Vite support for Electron is now available. The electron-vite package brings the speed and capabilities of Vite to Electron app development along with some new Electron-specific capabilities. 

In this article, we’ll discuss electron-vite and investigate how it improves Electron build performance at development time. Then, we’ll get hands-on with electron-vite by building a simple Electron app with a React frontend. 

_Jump ahead:_

*   [Prerequisites](#prerequisites)
*   [What is Electron?](#what-is-electron)
*   [What is Vite?](#what-is-vite)
*   [Why do we need electron-vite?](#why-need-electron-vite)
*   [Electron app project overview](#electron-app-project-overview)
*   [Creating a web app with Vite and React](#creating-web-app-vite-react)
*   [Converting to an electron app](#converting-electron-app)
*   [Adding native integration](#adding-native-integration)
*   [Experiencing the hot reload](#experiencing-hot-reload)
*   [Considering life without electron-vite](#considering-life-without-electron-vite)

## Prerequisites

All the code you need for the tutorial portion of this article is included below, but you’ll also need the following to easily follow along:

*   A high-level understanding of Electron and React
*   Node.js v18 or higher installed on your machine
*   Your favorite code editor

## What is Electron?

Electron is a framework that enables the development of cross-platform desktop applications with native integration to the underlying operating system. Electron apps consist of two main components:

*   **An embedded Node.js process:** referred to as the main process; backend JavaScript code can run on this process and interact with the operation system in various ways
*   **An embedded Chromium browser process:** referred to as the renderer process; this process can run a web app built with virtually any frontend framework like React

Electron enables frontend web developers to use existing web application code, tools, and skills to develop desktop applications that feel like native applications. JavaScript can be used to integrate with native capabilities like windows, menus, dialogs, and notifications.

## What is Vite?

[Vite is a frontend web development tool](https://blog.logrocket.com/getting-started-with-vite/) that improves the frontend development experience. Vite replaces, and in some cases improves upon, capabilities provided by other commonly used web development tools like react-scripts (based on webpack), Rollup, or Parcel. 

Vite differentiates itself from other dev tools with its speed when running a local server at development time. Vite splits your application into dependencies (i.e., node_modules) and source code. Dependencies are pre-bundled using esbuild, resulting in an extremely fast build process compared to other tools like webpack. 

Vite’s source code is not bundled and is served directly to the browser using native ESM support. Minimal processing of the source code is required before serving it to the browser, resulting in fast cold starts and extremely fast hot reloading when you make source code changes. You can read more about Vite’s dev server approach [in this guide](https://vitejs.dev/guide/why.html).

## Why do we need electron-vite? <a name="why-need-electron-vite">

[electron-vite](https://electron-vite.org/) brings Vite capabilities to Electron app development. It uses Vite at its core to build and serve web applications running in the Electron-embedded Chromium browser (renderer process). 

electron-vite extends some of Vite’s best features to improve the development experience for Electron application code. For example, this build tool:

*   enables [hot reloading](https://electron-vite.org/guide/hot-reloading.html) when the main and preload scripts are modified
*   provides a sensible default configuration for building Electron apps
*   provides [asset handling](https://electron-vite.org/guide/assets.html) for static assets used by the main process

It also provides additional features that tend to be required specifically when developing Electron apps. For example, electron-vite enables you to [protect your source code](https://electron-vite.org/guide/source-code-protection.html) by compiling it into V8 bytecode before distributing it to clients. 

Now, let’s get hands-on and actually build something with Electron, React, and electron-vite.

## Electron app project overview <a name="electron-app-project-overview">

In this tutorial, we’ll build a simple image filter desktop app that can open local image files and apply a few filters to the files. 

The instructions below were tested on a Windows device but should also work on Mac or Linux. electron-vite and Electron both provide inbuilt cross-platform support. 

Here’s what the final version of our app will look like: ![Electron React Electron-Vite Image Filter App](http://blog.logrocket.com/wp-content/uploads/2023/08/electron-react-electron-vite-image-filter-app.png)

## Creating a web app with Vite and React

To start our project, we need to create the web app that we will eventually run within Electron’s embedded browser. Let’s use Vite with the React template to generate starter code:

```bash
npm create vite@latest image-filter-app -- --template react
cd image-filter-app
```

Next, install some npm packages to help build the user interface and implement the image filter capability:

```bash
# to implement web user interface:
npm install @mui/material @mui/icons-material @emotion/react @emotion/styled

# to implement image filter capability:
npm install react-image-filter
```

Now, start the Vite dev server:

```bash
npm run dev
```

Vite’s dev server enables lightning-fast cold starts and hot reloading of changes. In the next few steps, you‘ll experience this speed as we implement the user interface. This is a good time to set up your code editor and browser windows side-by-side to appreciate just how fast your changes are reflected in the browser. 

Replace the contents of the `/src/App.jsx` file with this code:

```javascript
import Button from '@mui/material/Button';
import LandscapeIcon from '@mui/icons-material/Landscape';
import FileOpenIcon from '@mui/icons-material/FileOpen';
import { AppBar, Box, Container, CssBaseline, IconButton, Toolbar, Typography } from '@mui/material';
import ImageFilter from 'react-image-filter';
import { useState } from 'react';

function App() {
  const [imageFilter, setImageFilter] = useState(undefined);
  const [imageUrl, setImageUrl] = useState('https://source.unsplash.com/RZrIJ8C0860');

  async function onOpenFileClick() {
    // TODO
  };

  return (
    <>
      <CssBaseline />
      <Box sx={{
        flexGrow: 1,
        whiteSpace: 'nowrap',
        button: {
          color: 'inherit',
        }
      }}>
        <AppBar position="static">
          <Container>
            <Toolbar>
              <LandscapeIcon sx={{ mr: 1 }} />
              <Typography sx={{
                mr: '0.5em',
                fontSize: '1.4em',
                flexGrow: 1
              }}>Image Filter</Typography>
              <Button onClick={() => setImageFilter(undefined)}>Original</Button>
              <Button onClick={() => setImageFilter('invert')}>Invert</Button>
              <Button onClick={() => setImageFilter('sepia')}>Sepia</Button>
              <Button onClick={() => setImageFilter('duotone')}>Neon</Button>
              <IconButton type='button' color='inherit' onClick={onOpenFileClick}>
                <FileOpenIcon />
              </IconButton>
            </Toolbar>
          </Container>
        </AppBar>
      </Box>
      <Box sx={{ textAlign: 'center' }}>
        <ImageFilter
          image={imageUrl}
          alt="image to be styled"
          filter={imageFilter}
          colorOne={[104, 255, 0]}
          colorTwo={[255, 0, 92]}
          style={{ margin: '2em' }}
        />
      </Box>
    </>
  );
}

export default App;
```

You don’t need to pay much attention to the code in the `App.jsx` file. It really just contains some basic UI components and uses the [react-image-filter](https://www.npmjs.com/package/react-image-filter) package to apply filters to the loaded image. 

You may notice that the `onOpenFileClick` function is not implemented yet. Don’t worry, we‘ll implement this later with some native operating system integration using the Electron API. 

Next, replace contents of the `/src/main.jsx` file with this code:

```javascript
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
```

Save all your changes and watch the app reload in the browser in just milliseconds. Try making some changes to the `App.jsx` file to experience Vite’s speed. Once you’re ready to proceed, stop the Vite dev server.

## Converting to an Electron app

Now we need to convert our web app into an Electron desktop app built with electron-vite. This may sound challenging, but it is surprisingly simple. 

Install the Electron and electron-vite packages as dev dependencies:

```bash
npm install electron electron-vite --save-dev
```

Update the scripts in the `package.json` file to use electron-vite, instead of Vite, and to run the dev server, build, and preview the production build:

```json
"scripts": {
    "dev": "electron-vite dev -w",
    "build": "electron-vite build",
    "preview": "electron-vite preview"
},
```

Notice that the dev script includes the `-w` option, which tells electron-vite to watch for changes to Electron main and preload scripts and reload them automatically. Changes to web application code running on the renderer process will also be reloaded automatically thanks to the underlying Vite dev server included in electron-vite. 

Next, we’ll restructure the source code files and folders in the project. Although this is not absolutely necessary, in order to minimize the amount of configuration required to build the app with electron-vite, we‘ll follow the recommended folder convention documented [here](https://evite.netlify.app/guide/dev.html). 

Make the following changes to the project:

1.  Create an `src/renderer/src` folder
2.  Move the contents of the `src` folder to the `src/renderer/src` folder
3.  Move the `index.html` file in the root of the project to the `src/renderer` folder
4.  Create an `src/main` folder
5.  Create an empty `main.js` file in the `src/main` folder
6.  Create an `src/preload` folder
7.  Create an empty `preload.js` file in the `src/preload` folder

Here’s what the project should look like before and after you restructure it: ![Electron App Before After Restructuring](http://blog.logrocket.com/wp-content/uploads/2023/08/electron-app-before-after-restructuring.png)  Now, add an `electron.vite.config.js` file to the root of the project with this code:

```javascript
import { defineConfig } from "electron-vite";
import react from '@vitejs/plugin-react';

export default defineConfig({
    publicDir: false,
    main: {},
    preload: {},
    renderer: {
        plugins: [react()]
    }
});
```

The `react` plugin is included in the electron-vite renderer configuration so that we can take advantage of React’s [Fast Refresh](https://www.npmjs.com/package/react-refresh) feature at development time. Fast Refresh allows us to edit React components in a running application without losing their state. 

Add this code to the `/src/main/main.js` file:

```javascript
import { app, BrowserWindow } from 'electron';
import * as path from 'path';

let mainWindow;

function createWindow() {
  mainWindow = new BrowserWindow({});

  // Vite dev server URL
  mainWindow.loadURL('http://localhost:5173');
  mainWindow.on('closed', () => mainWindow = null);
}

app.whenReady().then(() => {
  createWindow();
});

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('activate', () => {
  if (mainWindow == null) {
    createWindow();
  }
});
```

The code in the `main.js` file creates a basic cross-platform window that renders our web app running from the Vite dev server at `localhost:5173`. 

Now, make the following changes to the `package.json` file:

1.  Remove `"type": "module"` if present
2.  Add a main entry point to tell Electron which script to load for the main process: `"main": "./out/main/main.js"`

Our project is ready to run as an Electron app:

```bash
npm run dev
```

The app will start up in a native window with a default menu and the embedded browser running the web app. You can even open up browser developer tools from the default menu: ![Image Filter App Running As Electron App](http://blog.logrocket.com/wp-content/uploads/2023/08/image-filter-app-running-electron-app.png)

## Adding native integration

So far, we’ve built a normal web application that runs in an embedded browser in Electron. Now, we’ll use Electron’s native integrations to connect the open image button in the web app to a native file selection dialog provided by the underlying operating system. 

This will allow the web app to open files directly on the user’s local file system. Normally the image would need to be uploaded to a web server before it can be rendered and manipulated in the browser. 

Make the following changes to the Electron main process script at `/src/main/main.js`:

1.  Import the `dialog` object from Electron
2.  Add a new `handleFileOpen` function that handles the output of the file selection dialog
3.  Update the `whenReady` event handler to connect the new `handleFileOpen` function to the dialog `openFile` event
4.  Update the `createWindow` function to hook up a preload script and disable web security on the main `BrowserWindow` instance

After making these changes, your `/src/main/main.js` script should look like this:

```javascript
import { app, BrowserWindow, dialog, ipcMain } from 'electron';
import * as path from 'path';

let mainWindow;

async function handleFileOpen() {
  const { canceled, filePaths } = await dialog.showOpenDialog({});
  if (!canceled) {
    return filePaths[0];
  }
}

function createWindow() {
  mainWindow = new BrowserWindow({
    webPreferences: {
      preload: path.join(__dirname, '../preload/preload.js'),
      webSecurity: false
    }
  });

  // Vite DEV server URL
  mainWindow.loadURL('http://localhost:5173');
  mainWindow.on('closed', () => mainWindow = null);
}

app.whenReady().then(() => {
  ipcMain.handle('dialog:openFile', handleFileOpen);
  createWindow();
});

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('activate', () => {
  if (mainWindow == null) {
    createWindow();
  }
});
```

_**N.B.,** the `webSecurity` setting is enabled by default but we are disabling it to keep this example simple. When this setting is enabled, the embedded browser is run in isolation and is unable to access the local file system. Disabling web security to gain access to the file system is not the correct approach for a real application. There are other, better ways to do this. Refer to [Electron issue 23393](https://github.com/electron/electron/issues/23393#issuecomment-623694579) for examples._ 

Now, copy the following code to the `/src/preload/preload.js` file:

```javascript
import { contextBridge, ipcRenderer } from 'electron';

contextBridge.exposeInMainWorld('electronAPI', {
    openFile: () => ipcRenderer.invoke('dialog:openFile')
});
```

The preload script contains the bridge that connects the web app running on the renderer process to native capabilities from the underlying operating system. The `exposeInMainWorld` function is used to explicitly expose the Electron APIs that the web app can call. 

Finally, we need to implement the `onOpenFileClick` function in the `/src/renderer/src/App.jsx` file. Replace it with this code to call the Electron API we exposed in the preload script:

```javascript
async function onOpenFileClick() {
  const filePath = await window.electronAPI.openFile();
  setImageUrl(filePath);
};
```

Use the `npm run dev` command to run the app. Then, test the open image button. Now you can open an image on your local file system and apply filters to it.

## Experiencing the hot reload <a name="experiencing-hot-reload">

To experience the electron-vite hot reload, try making the below changes to your app:

*   Modify the title in the `Typography` tag in the `App.js` file to something other than “Image Filter”. After saving the change, the embedded browser will load the change nearly instantaneously thanks to Vite’s super-fast dev server with hot module reloading
*   Add the `autoHideMenuBar: true` option to the `BrowserWindow` object in the `/src/main/main.js` file. This option will hide the native menu bar by default. After saving the change, the whole app will automatically restart with the menu bar hidden thanks to electron-vite hot reload. Press the **Alt** key (on Windows) to bring the menu bar back

_**N.B.,** ensure you start the app in dev mode with the `npm run dev` command before making any changes_

```javascript
  mainWindow = new BrowserWindow({
    webPreferences: {
      preload: path.join(__dirname, '../preload/preload.js'),
      webSecurity: false
    },
    autoHideMenuBar: true
  });
```

## Considering life without electron-vite

Now that you’ve experienced building and running an Electron app with electron-vite, consider what the developer experience would be like without this build tool. 

You could build the React portion of this app using a variety of tools including Vite. You could still take advantage of features like Vite’s hot module reload to make changes to the web app and see them reflected in Electron’s embedded browser quickly while still maintaining app state. This does not require electron-vite. 

However, the developer experience begins to degrade as soon as you make changes to the Electron main or preload scripts. Without electron-vite, any change to these would require the entire app to be restarted manually. 

This is particularly cumbersome when working on an app that has a lot of native integration or code running on the main process. You would also miss out on all other aforementioned benefits of electron-vite, like source code protection.

## Conclusion

Wondering if you should use electron-vite? Absolutely — if you value your time! electron-vite brings the fast, developer-friendly experience that developers have come to expect from Vite plus several other Electron-specific features you will probably eventually need before you ship your app. 

I hope you enjoyed this article. The full source code for the image filter app demonstrated in this tutorial is available on [GitHub](https://github.com/briandesousa/electron-react-image-filter-app).

---

## Get set up with LogRocket's modern error tracking in minutes:

1. Visit https://logrocket.com/signup/ to get an app ID.
2. Install LogRocket via NPM or script tag. `LogRocket.init()` must be called client-side, not server-side.

npm:
```bash
$ npm i --save logrocket 

// Code:

import LogRocket from 'logrocket'; 
LogRocket.init('app/id');
```
Script tag:
```javascript
Add to your HTML:

<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
```

3.(Optional) Install plugins for deeper integrations with your stack:
*   Redux middleware
*   ngrx middleware
*   Vuex plugin

[Get started now](https://lp.logrocket.com/blg/signup)