Build a Secure Desktop App with Electron Forge and React

In this article, we’ll create a simple desktop application using Electron and React. It will be a small text editor called “scratchpad” that automatically saves changes as you type, similar to FromScratch. We’ll pay attention to making the application secure by using Electron Forge, the up-to-date build tool provided by the Electron team.

Electron Forge is “a complete tool for creating, publishing, and installing modern Electron applications”. It provides a convenient development environment, as well as configuring everything needed for building the application for multiple platforms (though we won’t touch on that in this article).

We’ll assume you know what Electron and React are, though you don’t need to know these to follow along with the article.

You can find the code for the finished application on GitHub.

Setup

This tutorial assumes that you have Node installed on your machine. If that’s not the case, please head over to the official download page and grab the correct binaries for your system, or use a version manager such as nvm. We’ll also assume a working installation of Git.

Two important terms I’ll use below are “main” and “renderer”. Electron applications are “managed” by a Node.js JavaScript file. This file is called the “main” process, and it’s responsible for anything operating-system related, and for creating browser windows. These browser windows run Chromium, and are referred to as the “renderer” part of Electron, because it’s the part that actually renders something to the screen.

Now let’s begin by setting up a new project. Since we want to use Electron Forge and React, we’ll head over to the Forge website and look at the guide for integrating React.

First off, we need to set up Electron Forge with the webpack template. Here’s how we can do that in one terminal command:

$ npx create-electron-app scratchpad --template=webpack

Running that command will take a little while as it sets up and configures everything from Git to webpack to a package.json file. When that’s done and we cd into that directory, this is what we see:

➜  scratchpad git:(master) ls
node_modules
package.json
src
webpack.main.config.js
webpack.renderer.config.js
webpack.rules.js

We’ll skip over the node_modules and package.json, and before we peek into the src folder, let’s go over the webpack files, since there are three. That’s because Electron actually runs two JavaScript files: one for the Node.js part, called “main”, which is where it created browser windows and communicates with the rest of the operating system, and the Chromium part called “renderer”, which is the part that actually shows up on your screen.

The third webpack file — webpack.rules.js — is where any shared configuration between Node.js and Chromium is set to avoid duplication.

Okay, now it’s time to look into the src folder:

➜  src git:(master) ls
index.css
index.html
main.js
renderer.js

Not too overwhelming: an HTML and CSS file, and a JavaScript file for both the main, and the renderer. That’s looking good. We’ll open these up later on in the article.

Adding React

Configuring webpack can be pretty daunting, so luckily we can largely follow the guide to integrating React into Electron. We’ll begin by installing all the dependencies we need.

First, the devDependencies:

npm install --save-dev @babel/core @babel/preset-react babel-loader

Followed by React and React-dom as regular dependencies:

npm install --save react react-dom

With all the dependencies installed, we need to teach webpack to support JSX. We can do that in either webpack.renderer.js or webpack.rules.js, but we’ll follow the guide and add the following loader into webpack.rules.js:

module.exports = [
  ...
  
    test: /.jsx?$/,
    use: 
      loader: 'babel-loader',
      options: 
        exclude: /node_modules/,
        presets: ['@babel/preset-react']
      
    
  ,
];

Okay, that should work. Let’s quickly test it by opening up src/renderer.js and replacing its contents with the following:

import './app.jsx';
import './index.css';

Then create a new file src/app.jsx and add in the following:

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(<h2>Hello from React in Electron!</h2>, document.body);

We can test if that works by running npm start in the console. If it opens a window that says “Hello from React in Electron!”, everything is good to go.

You might have noticed that the devtools are open when the window shows. That’s because of this line in the main.js file:

mainWindow.webContents.openDevTools();

It’s fine to leave this for now, as it will come in handy while we work. We’ll get to main.js later on in the article as we configure its security and other settings.

As for the error and the warnings in the console, we can safely ignore them. Mounting a React component on document.body can indeed be problematic with third-party code interfering with it, but we’re not a website and don’t run any code that’s not ours. Electron gives us a warning as well, but we’ll deal with that later.

Building Our Functionality

As a reminder, we’re going to build a small scratchpad: a little application that saves anything we type as we type it.

To start, we’ll add CodeMirror and react-codemirror so we get an easy-to-use editor:

npm install --save react-codemirror codemirror

Let’s set up CodeMirror. First, we need to open up src/renderer.js and import and require some CSS. CodeMirror ships with a couple of different themes, so pick one you like, but for this article we’ll use the Material theme. Your renderer.js should now look like this:

import 'codemirror/lib/codemirror.css';
import 'codemirror/theme/material.css';
import './app.jsx';
import './index.css';

Note how we import our own files after the CodeMirror CSS. We do this so we can more easily override the default styling later.

Then in our app.jsx file we’re going to import our CodeMirror component as follows:

import CodeMirror from 'react-codemirror';

Create a new React component in app.jsx that adds CodeMirror:

const ScratchPad = () => 
  const options = 
    theme: "material"
  ;

  const updateScratchpad = newValue => 
    console.log(newValue)
  

  return <CodeMirror
    value="Hello from CodeMirror in React in Electron"
    onChange=updateScratchpad
    options=options />;

Also replace the render function to load our ScratchPad component:

ReactDOM.render(<ScratchPad />, document.body);

When we start the app now, we should see a text editor with the text “Hello from CodeMirror in React in Electron”. As we type into it, the updates will show in our console.

What we also see is that there’s a white border, and that our editor doesn’t actually fill the whole window, so let’s do something about that. While we’re doing that, we’ll do some housekeeping in our index.html and index.css files.

First, in index.html, let’s remove everything inside the body element, since we don’t need it anyway. Then we’ll change the title to “Scratchpad”, so that the title bar won’t say “Hello World!” as the app loads.

We’ll also add a Content-Security-Policy. What that means is too much to deal with in this article (MDN has a good introduction, but it’s essentially a way to prevent third-party code from doing things we don’t want to happen. Here, we tell it to only allow scripts from our origin (file) and nothing else.

All in all, our index.html will be very empty and will look like this:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Scratchpad</title>
    <meta http-equiv="Content-Security-Policy" content="script-src 'self';">
  </head>
  <body></body>
</html>

Now let’s move to index.css. We can remove everything that’s in there now, and replace it with this:

html, body 
  position: relative;
  width:100vw;
  height:100vh;
  margin:0;
  background: #263238;


.ReactCodeMirror,
.CodeMirror 
  position: absolute;
  height: 100vh;
  inset: 0;

This does a couple of things:

  • It removes the margin that the body element has by default.
  • It makes the CodeMirror element the same height and width as the window itself.
  • It adds the same background color to the body element so it blends nicely.

Notice how we use inset, which is a shorthand CSS property for the top, right, bottom and left values. Since we know that our app is always going to run in Chromium version 89, we can use modern CSS without worrying about support!

So this is pretty good: we have an application that we can start up and that lets us type into it. Sweet!

Except, when we close the application and restart it again, everything’s gone again. We want to write to the file system so that our text is saved, and we want to do that as safely as possible. For that, we’ll now shift our focus to the main.js file.

Now, you might have also noticed that even though we added a background color to the html and body elements, the window is still white while we load the application. That’s because it takes a few milliseconds to load in our index.css file. To improve how this looks, we can configure the browser window to have a specific background color when we create it. So let’s go to our main.js file and add a background color. Change your mainWindow so it looks like this:

const mainWindow = new BrowserWindow(
  width: 800,
  height: 600,
  backgroundColor: "#263238",
);

And now when you start, the flash of white should be gone!

Continue reading
Build a Secure Desktop App with Electron Forge and React
on SitePoint.