React + Electron + Typescript — A Dev Experience (part 2)

Bartek Polanczyk
The Startup
Published in
8 min readAug 27, 2020

In the previous part of the article (https://medium.com/@SzybkiSasza/react-electron-typescript-a-dev-experience-part-1-f507e98dd4d9) we created a foundation for the future app:

  • Preparing Typescript-based Create React App
  • Adding Material UI
  • Adding example service for storing image files (in browser storage, using localforage).
  • Coinfiguring ESLint and Stylelint for properly linting whole codebase

The next step is adding Electron to the mix — our goal is to create a unified experience for seamlessly working with both Electron and React, without the hassle of running multiple scripts at different times.

Just give me the code!

You can find the workshop code at https://github.com/SzybkiSasza/gdg-react-workshop

Preparing the Electron

Before we start, we need to install all the dependencies that are needed for Electron, in particular — main package and the tool called Electron Builder that creates final platform bundle:

yarn add --dev electron electron-builder cross-env nodemon foreman

We also installed some additional tools:

  • CrossEnv that’ll allow for injecting environment variables to NPM scripts regardless of operating system
  • Nodemon for observing changes in the code and re-running the app on every code change (occurring either in Create React App or Electron)
  • Foreman for gluing all the scripts together so that they can be run in parallel from one script

Let’s electron-ify the app!

Using Electron is simple — it boils down to a few steps:

  1. Creating the so-called Application (a special NodeJS wrapper around Chromium)
  2. Managing Application lifecycles (e.g. activating on Mac, App Ready, App windows closed) that are closely tied to system events
  3. In proper lifecycle methods, building and destroying Browser Window (you can think of it as a headless Chromium browser running in a dedicated window)

TL;DR — you spawn the Browser inside the App window and display content that is served by React (either by React Dev server in development or ready-to-serve production bundle).

To start with, we’ll need a few files:

  • new tsconfig.json that will define Typescript pipeline for Electron files (it’ll be much simpler than the one used by React!)
  • main.ts file housing our Electron App
  • menu.ts file that will be a simple configuration for our menu (things that usually show in the top bar of the app or in case of some environments — at the top of the active display)
  • connect-electron.ts file that will tell our scripts when Electron can connect to React Dev Server so that Electron window is ready on time

We’ll put all the files in a new directory so that our project structure looks like this:

Final project structure

In this structure:

  • build will represent transpiled electron files ready to be bundled as a standalone system installer
  • dist will hold both Electron and Create React app production files
  • electron is a container for all the Electron-related code
  • public and src are CRA-related directories

Running the first desktop App

Let’s start by running the Electron and Create React App separately — we’ll focus on optimizing the pipeline later.

We’ll begin with creating tsconfig.json for the Electron App:

The most important part of the config is information about where to put the output of the Typescript compilation (`build/electron`) and module format (different than the one for CRA).

Having that ready, let’s prepare a basic Electron bootstrap code in main.ts :

main.ts

For now, we’ll skip setting up the menu (Electron will auto-generate the default menu for us) — you can check the menu structure here: https://github.com/SzybkiSasza/gdg-react-workshop/blob/master/electron/menu.ts.

The code below is rather straightforward:

  1. Defines the createWindow function that will set-up the App
  2. Attaches the function to proper lifecycles of the App (where activate is Mac-only state of clicking on an inactive app icon, hence — we re-create window then)

Let’s focus on specific parts:

  • Function checks if one has an external display connected and displays the app on it. Otherwise, it’s displayed on the main screen
  • The app itself occupies 1280x1024 pixels (some libraries allow for remembering last window size, but it’s out of the scope of this article)
  • Bootstraps the app by calling loadURL , using either environment variable (for dev mode) or the location of CRA bundle entry file ( index.html in the same directory as the transpiled file, e.g. dist/index.html )

To run the code we just need to inform Electron where to find the final main.js (note the extension!) transpiled file. We’ll do it by creating a special entry in package.json :

...
"main": "build/electron/main.js",
...

This tells the Electron to run the file in that particular directory, but we still need to build it. To do so, we’ll run three separate scripts:

  • cross-env BROWSER=none react-scripts start — this will run react itself, informing it that it’s not run in standard browser context (hence BROWSER=none )
  • tsc --project electron/tsconfig.json that will transpile Electron files and put them in build directory
  • electron . that will instruct Electron to look for the config in the current directory (project directory) and bootstrap the app based on the main entry

For now, Electron expects that main.js file will be already available — wait for the Typescript to transpile the file first!

This should be enough to get the app up and running — the result should look like that:

Main Window

Current dev experience isn’t the best one — we need to run 3 separate scripts in order to run the app. Let’s streamline it a bit!

Dev pipeline — one script to rule them all!

Let’s start with development —to run the whole environment in one go we have to run all the three scripts from the previous step together. We’ll use Foreman for that. Create a new file in the project tree called Procfile and put the following content inside:

react: npm run start-react
start-electron: npm run start-electron
watch-electron: npm run watch-electron

This file tells the Foreman what should be run in parallel. We run all three scripts together. Names before colons will be the ones showing up in Foreman logs as script descriptors.

Let’s create each of the scripts in package.json now:

...
"start-react": "cross-env BROWSER=none react-scripts start",
"start-electron": "ts-node --skip-project electron/connect-electron.ts",
"watch-electron": "tsc --watch --project electron/tsconfig.json"
...

Each of the scripts is related to previously mentioned tasks, but we still need to properly bootstrap electron app using connect-electron.ts . It’ll take care of waiting until React is ready and restarting the Electron then:

connect-electron.ts

As Create React App bootstraps React Dev Server on the particular port, we can assume once there’s TCP connection on that port, React is ready.

Once the connection is available, we use Nodemon to run the Electron, based on previously built files.

There’s a slight inconvenience regarding that implementation — there won’t be main.js file available at the first run and therefore — Electron script may fail. Just run it again and it’ll work properly from now on.

A small note on using Nodemon to reload Electron — there are special libraries that allow for reloading Electron code from within Electron (without using Nodemon), but they cause a bit of a hassle in Windows environments — they spawn a new CMD window every time Electron is being reloaded. To circumvent it, we use a slightly slower method of re-running the code on file system level, using Nodemon.

The final touch — Foreman script

As we already prepared Foreman config and Electron bootstrap script, we can run all the scripts together, using Foreman. Add another script to your package.json to be able to do so:

"start": "nf start -p 3000"

That’s all there is! Now Foreman should spawn all the scripts in parallel and prepare the app:

Foreman console output

Note how each of the scripts is highlighted with unique color and prefix — thanks to that you can easily tell what’s going on once one of the scripts throws the error.

It’s all that is! We use one script to run all the dev pipelines together and can listen to changes in any of the files live! Time to prepare a production build.

Ready to ship — adding the installer

We’re one step away from being able to ship our app — it’s time to build the application and bundle it as a standalone installer. Similar to the DEV pipeline, we need to run three scripts together:

  • CRA builder
  • Electron Typescript transpiler
  • Electron Builder script that will bundle the application

In order to be able to bundle the app, add following config to your package.json :

"build": {
"appId": "com.electron.gdg-react-workshop",
"directories": {
"buildResources": "public"
}
}

This describes Electron project for the Bundler — tells what’s the ID of the app and where do static resources reside. This is enough for this article, you can find more at https://github.com/electron-userland/electron-builder .

Now it’s time to prepare final build scripts. Final configuration may look like that (unrelated scripts removed for brevity):

"scripts": {
"build": "react-scripts build",
"start": "nf start -p 3000",
"test": "react-scripts test",
"build-electron": "tsc --project electron/tsconfig.json",
"start-electron": "ts-node --skip-project electron/connect-electron.ts",
"watch-electron": "tsc --watch --project electron/tsconfig.json",
"start-react": "cross-env BROWSER=none react-scripts start",
"dist": "npm run build && npm run build-electron && electron-builder build -c.extraMetadata.main=build/electron/main.js"
},

By running yarn dist we’ll prepare the final production build. Once it’s finished, you can find the app installer inside dist directory:

Installer ready to use

Now we can run the installer! It’ll unpack the App and make it available in your system environment!

To sum up…

We added Electron to the project and configured whole environment to be able to bootstrap whole app with one script. Finally, we created the installer/builder script that prepares the installer that can be used as any other setup/wizard.

Whole code can be found here: https://szybkisasza.github.io/gdg-react-workshop

Feel free to port the code according to your needs and migrate it however you want, as long as you give me kudos 😉

--

--

Bartek Polanczyk
The Startup

Senior Engineer @sumologic, traveler, diver, homegrown musician