Desktop Apps with Electron + Next.JS (without Nextron)

rbfraphael

Raphael Batista Fontão

Posted on November 29, 2023

Desktop Apps with Electron + Next.JS (without Nextron)

Next.JS inside Electron

A bit of story first

Some time ago I've started a personal project called Local.Host, which was a replacement alternative for XAMPP, for PHP developers using Windows. Due to my extensive knowledge of web development, I decided to use Electron for building the desktop app, alongside with some Windows' CMD commands and tricks.

After some search, I've found Nextron, a good Next.JS + Electron boilerplate. But, today, I've noticed by some friends that my software is a bit heavy, sometimes freezing. Understanding more about Electron, I've noticed that Nextron comes with an old version of both Electron and Next.JS, and some "internal" mechanisms used are causing slow downs on my application.

To not fully rewrite the app, I tried to "create" my own Next.JS + Electron project, with latest versions of both and, obviously, more lightweight than Nextron.

So, now, I have a (I personally consider it) very solid guide for creating desktop apps with these techs. And, of couse, I decided to share it with you, because I've found a lot of people that are searching for something like that and are all with the same opinion about Nextron.

The guide

Step 1 - Start a Next.JS project

Start with Next.JS. First, run the traditional command npx create-next-app@latest <your_project_name>. Set the preferences you want durint setup. The only setting you need to take care is about using App Router. Currently, I personally choose "No" to keep using the old-but-gold Pages Router, and it's the way I've used.

npx create-next-app@latest electron-nextjs-project
Enter fullscreen mode Exit fullscreen mode

Step 2 - Install Electron and other dependencies

cd into your project folder (in this example, electron-nextjs-project) and install the following dependencies, with both following commands:

Dev dependencies:

npm install --save-dev electron electron-builder concurrently
Enter fullscreen mode Exit fullscreen mode

Project dependencies:

npm install electron-serve
Enter fullscreen mode Exit fullscreen mode

Step 3 - Setting up package.json

Start opening the package.json file with your preferred text editor or IDE. You need to modify the build and dev scripts, and add the main property. The concurrently we installed before will be used to run both Next.JS and Electron in parallel during the development, and you need to point the main script to the entrypoint of your Electron application. Also, we need to add "author" and "description" attributes, both needed when building the application executables.

{
  ...
  "main": "main/main.js",
  "author": "RBFraphael",
  "description": "Electron + NextJS example project",
  "scripts": {
    "dev": "concurrently -n \"NEXT,ELECTRON\" -c \"yellow,blue\" --kill-others \"next dev\" \"electron .\"",
    "build": "next build && electron-builder",
    ...
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

package.json

As you can see, when developing with npm run dev will start both Next.JS and Electron (you can find more details about Concurrently here), and, when building your application with npm run build it will build the Next.JS files, then the Electron application (find more information about Electron Builder here).

Step 4 - Configuring Next.JS

Since we're using Next.JS as the starting point, we should have a next.config.js file in the root of our project. In this file, we need to set the output mode to export, and disable the image optimization. So, inside this file, add the following params to the exported nextConfig object.

const nextConfig = {
  ...
  output: "export",
  images: {
    unoptimized: true
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

next.config.js

Step 5 - Electron bootstrap

Now, we will code the Electron part of our application. Start creating a folder called main, and two files inside that folder: main.js (as we set on the main key of our package.json) and preload.js (for exposing Electron to front-end), and insert the following content in the main.js file.

main.js

const { app, BrowserWindow } = require("electron");
const serve = require("electron-serve");
const path = require("path");

const appServe = app.isPackaged ? serve({
  directory: path.join(__dirname, "../out")
}) : null;

const createWindow = () => {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, "preload.js")
    }
  });

  if (app.isPackaged) {
    appServe(win).then(() => {
      win.loadURL("app://-");
    });
  } else {
    win.loadURL("http://localhost:3000");
    win.webContents.openDevTools();
    win.webContents.on("did-fail-load", (e, code, desc) => {
      win.webContents.reloadIgnoringCache();
    });
  }
}

app.on("ready", () => {
    createWindow();
});

app.on("window-all-closed", () => {
    if(process.platform !== "darwin"){
        app.quit();
    }
});
Enter fullscreen mode Exit fullscreen mode

main.js

As we can analyse, this code will use electron-serve to properly serve static files from the out/ folder, but only when our app is packaged (builded, in production, compiled, whatever you want). While not packaged, we will run our app through the http://localhost:3000 URL, which is the default URL for Next.JS projects. Also, we already prepared the script to load our preload.js file. Another point of this scripts is the "did-fail-load" event while development. We added it because sometimes Electron may start faster than Next.JS (remember they are called together with concurrently), so, in those moments, it will trigger an error that "URL cannot be loaded" or someting like that, and will give us only a blank screen. With this event implemented on our script, when this occurs, it will automatically reload the app content, until success when showing Next.JS content.

Step 6 - Setting up the preload script

Now, we just need to expose the Electron for the React/Next.JS part of our application. It's very useful for using IPC messages, for example.

Inside the main/preload.js file we created before, insert the following content:

const { contextBridge, ipcRenderer } = require("electron");

contextBridge.exposeInMainWorld("electronAPI", {
    on: (channel, callback) => {
        ipcRenderer.on(channel, callback);
    },
    send: (channel, args) => {
        ipcRenderer.send(channel, args);
    }
});
Enter fullscreen mode Exit fullscreen mode

preload.js

This way, we can call window.electronAPI.on on Next.JS components to handle IPC data that comes from the "back-end" of our application, and window.electronAPI.send to send data to the "back-end" of our application.

Step 7 - Testing

Finally, we can just run npm run dev on our terminal. If all things are well configured, we should see a beautiful Electron window with the starter content of Next.JS. Also, the built-in DevTools shoud appear. And, in the running terminal, we can see the debug from both Next and Electron, tagged with different colors too. Now is time to develop our application. Remember that we have hot reload for front-end (the renderer/Next part of our app), but, when making changes into the "back-end" (the main/Electron part of our application) we need to stop the whole application (a simple Ctrl+C on the terminal do the job) then run it again.

Next.JS inside Electron

Step 8 - Building the executables

Now it's time to build our application executables. We're using electron-builder to handle that for us. Start creating a file called electron-builder.yaml on the root of our project. Then, inside this file, put the configuration for building the application, according to the official electron-builder documentation. You can find an example below:

appId: "io.github.rbfraphael.electron-next"
productName: "Electron Next.JS"
copyright: "Copyright (c) 2023 RBFraphael"
win:
  target: ["dir", "portable", "zip"]
  icon: "resources/icon.ico"
linux:
  target: ["dir", "appimage", "zip"]
  icon: "resources/icon.png"
mac:
  target: ["dir", "dmg", "zip"]
  icon: "resources/icon.icns"

Enter fullscreen mode Exit fullscreen mode

electron-builder.yaml

After properly specifying the needed options on electron-builder.yaml file, you just need to run npm run build on your terminal. The Next.JS static files will be generated and exported to the out/ directory, then the Electron application will be compiled, and saved to the dist/ directory.

Generated files

Now, when you run your application with the exported files, you should see a Electron window with your Next.JS app, now without the built-in DevTools.

Final application

Conclusion

Using Next.JS to build desktop application with Electron is a good idea, specially when you want to use React alongside with Next.JS features, such as easy routing. And it shouldn't be a pain, and using Nextron it could be. So, the best approach (I personally believe) is to start from scratch. In this guide we learned an approach to handle that.

Hope this guide may help somebody :)

Thanks!

💖 💪 🙅 🚩
rbfraphael
Raphael Batista Fontão

Posted on November 29, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related