Since the first release of Electron in 2013 we've come a very long way: With the high goal of conquering native, the web's ambassadors (you and me) developed various useful projects for all kinds of use cases.

Nowadays, many of the daily tasks previously handled by native apps are now done by software that runs on top of Electron: Editing code (Atom), sending and receiving messages (WhatsApp, Slack) or emails (Nylas Mail) and even complex things like running commands (Hyper):

To sum this up, I don't think I have to tell you any more how important it is and how big its impact on our industry is today. And yet, it's still growing!

Of course, these achievements weren't completely free of charge:

We spent hours convincing our fellow coworkers that a rewrite was worth it. We held conferences and spread the word across the whole globe, so that all of us may have the ability to convert our app ideas into reality.

Some of us even contributed our own spare time and spent it reporting issues, fixing bugs and making Electron better!

Sure, all of this has been very difficult. But let's not rest now!

There's still a long road ahead of us: More operating systems and devices are waiting for us. We're barely halfway there. So much more to discover!

#Chapters

If you want, you can jump directly to a specific section. However, if you want to be guided through building an Electron app using Next.js from start to finish (recommended), simply skip to the next paragraph.

#Highlighted OSS Projects

If you want to jump right into some code, here are a few GitHub repositories (this article talks about many more, but the ones below are the most important):

#The Dream

But enough of the 👏 cheering 👏 now. With the above in mind, I'd like to introduce you to a new concept of building Electron apps:

So far, we've either been using vanilla JavaScript or some sort of custom Webpack/Babel/Gulp setup to handle building the renderer process.

At ZEIT, we've gathered a lot of experience with this topic in the past and discovered a much more efficient and more straightforward way to handle this: Next.js!

Yes, I'm serious. It's not just very good for sites and web apps, but it also makes creating Electron apps easier than ever before. That's because it allows us - as app developers - to abstract most of the complex development environment away into a tiny tool belt.

Once implemented, Next.js will take care of:

  • Handling the routing and prefetching of pre-defined pages
  • Transpiling, bundling and minifying your code
  • Splitting your code
  • Live-reloading all BrowserWindow instances that are pointing to Next.js pages
  • Styling components and pages using styled-jsx

In order for Next.js bundles to fit perfectly into how we're building Electron apps (and to make deploying easier), we also released a feature that lets you export static files for the use in the production version of your app.

And that's not even all yet!

So why can't we have these wonderful features inside an Electron app without a complicated configuration setup? Well... Starting today, it's actually possible! 🎉

#Making the Dream Come True

As the first step into the light, we'll spin up a fresh Electron boilerplate. This will make you understand how to create the perfect renderer process using Next.js, but also how to implement it into your existing application.

Thankfully, there's a suitable skeleton app which you can easily clone to your device (Git and Node.js need to be installed):

git clone https://github.com/electron/electron-quick-start

Once it has finished setting up a clone that you can interact with on your local device, move into the directory of the repository:

cd electron-quick-start

#Installing the Dependencies

Now that we got the boilerplate, let's make sure that the pre-defined dependencies are installed. To do so, you only need to run the following command:

npm install

Next, install the remaining dependencies that we'll need for the renderer. First, we'll start with the devDependencies: The core package of Next.js (used in development), react and react-dom (bundled with the production version of your app):

npm install next@beta react react-dom --save-dev

As the last installation step, we need to define electron-next, which ensures that Electron can handle Next.js' output in the renderer...

...and electron-is-dev, which allows us to change the main process' behaviour depending on the environment the application is running in (you'll understand why this is important later in this tutorial).

They will be used both in production and development, so they need to be installed as normal dependencies:

npm install electron-next electron-is-dev --save

#Testing the Boilerplate

Before we go on, I suggest ensuring that you've followed the instructions carefully. To do so, you can start the boilerplate application like this:

npm start

If you've done everything right, a window should open:

Saw it? Perfect! Now you can go on with the next section and skip the following sentence. If not, please repeat the steps from here.

#Preparing the New Renderer

Looks like we're good to go into real detail now. Are you excited? I am, at least! So let's not waste any more time and talk about how we can add our Next.js code to our Electron project.

For the module to have something to create a bundle from, we need to create a directory called "renderer" (I suggest calling the directory that contains the main process code "main"). Create it and then also move into it:

mkdir renderer && cd renderer

Inside this directory, you can build your file and folder structure just like you would do in a normal Next.js site. Take Now Desktop as an example.

For this tutorial however, we'll only create the most important things: An entry page and the configuration file that tells Next.js how to behave correctly. Let's start with the entry page (a file named start.js inside "pages"). For now, we'll just use basic JSX to print out "Ahoy, Next.js" inside an HTML tag:

export default () => (
  <span>This is Next.js speaking</span>
)

After you're done, create the configuration file named next.config.js. Inside it, you only need to do handle one thing:

module.exports = {
  exportPathMap() {
    // Let Next.js know where to find the entry page
    // when it's exporting the static bundle for the use
    // in the production version of your app
    return {
      '/start': { page: '/start' }
    }
  }
}

Now we're finished with preparing a basic renderer. In the next section, you'll learn what you need to do in order for your application to properly bundle the code we just added (a.k.a. how to make the main process handle it).

#Preparing the Main Process

In order for the renderer code to be built and reloaded automatically by Next.js, we firstly need to load electron-next (which we added as a dependency earlier) and call it inside the existing main.js file (contains the main process' code).

On the first line of the file, we'll import it:

const prepareNext = require('electron-next')

Then the only thing left is calling it. For this to happen, we need to replace the existing event listener (which creates a window once the app is ready)...

app.on('ready', createWindow)

...with one that does the same but also makes the main process handle the new renderer code before creating the window:

app.on('ready', async () => {
  await prepareNext('./renderer')
  createWindow()
})

Now the only thing left is pointing the existing BrowserWindow instance to the right URL depending on the environment the app is running in:

In development, it should show the hot-reloading and automatically re-building instance of Next.js and in production, it needs to serve the static renderer files that were exported in advance (we'll talk about the exporting soon).

So please move back to the first line and load electron-is-dev, a dependency we've added earlier in this section:

const isDev = require('electron-is-dev')

Next, switch to the line where the window content gets loaded using the .loadURL method and replace it with this:

let entry

if (isDev) {
  entry = 'http://localhost:8000/start'
} else {
  entry = url.format({
    pathname: path.join(__dirname, './renderer/start/index.html'),
    protocol: 'file:',
    slashes: true
  })
}

mainWindow.loadURL(entry)

Now, when running npm start to start the application, you should see this:

As you can see, Electron is now rendering the entry page that was built by Next.js behind the curtains. When making changes to the start.js file inside the "pages" directory, you'll notice that the code is being reloaded automatically.

From here on, you can start building a full-blown new renderer using Next.js inside the "renderer" directory – just like you would do it when building a web app! 😏

#Taking It to Production

When reading this section, you've already learned how to implement Next.js into an Electron application and take advantage of its development workflow. However, you still haven't understood how this works in the bundled production version of your application. The next few paragraphs will tell you.

The reason why electron-next doesn't have the same behaviour in the final app as in development is that opening a port (like Next.js does it for providing hot reloading and auto-building of your code) is generally not a good idea because it introduces a security vulnerability on the user's device.

It's therefore safe for development, but not for production.

What's much safer, however, is letting the BrowserWindow instances directly access the static files of your renderer. This is made possible by next export, a new sub command that we've introduced with Next.js 3.

For this example, we're going to use electron-builder, a widely used solution for packing your application. Please keep in mind that this is not a requirement - you can use any builder of your choice, as long as you configure it to do the same as what I'll show you now...

Firstly, install it from npm:

npm install electron-builder --save-dev

Then you need to modify the scripts property to include a two new values next to the existing start one:

"scripts": {
  "start": "electron .",
  "build": "next build renderer && next export renderer",
  "dist": "npm run build && build"
}
  • build will tell Next.js how to build and export your renderer
  • dist makes electron-builder run the build script before packing the app

Afterwards, the final touch of any code file in this tutorial will be to add a entirely new property to your package.json file: It tells electron-builder to not wrap the application's content into an ASAR archive. In addition, it also moves the output generated by next export into position:

"build": {
  "asar": false
},
"extraResources": [
  {
    "from": "renderer/out",
    "to": "app/renderer"
  }
]

The build property is only included because electron-next doesn't yet support ASAR archives. If you need this, please let me know, so that I can consider implementing it. Pull requests are also very welcome!

That's it! Now you can test your packaged application in production:

npm run dist

Isn't that wonderfully magic? ✨

Starting today, you can use Next.js to build wonderful Electron applications that will empower and maybe even entertain your audience - your call!

In order for you to better understand how all of this comes together in the end, I also created a boilerplate, which sums all of the stuff you've just learned up in one repository - feel free to clone it and try it out by yourself!

Also make sure to check out Next.js and electron-next. If there's anything missing or something needs that needs to be fixed, feel free to take part!

Now the only thing left for me to say is:

Thank you for taking the time to read this article! I'm sure that we have the collective power to bring all of the web's capabilities to native (...and beyond)! 🚀

Oh. And you can follow me on Twitter if you want to stay updated about how this technique evolves and when I discover new ways to make it even easier. Enjoy the rest of your day!