Desktop Apps with Electron + Next.JS (without Nextron)
Raphael Batista Fontão
Posted on November 29, 2023
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
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
Project dependencies:
npm install electron-serve
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",
...
}
...
}
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
}
...
}
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();
}
});
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);
}
});
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.
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"
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.
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.
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!
Posted on November 29, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.