React + Electron + Typescript — A Dev Experience (part 2)
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:
- Creating the so-called Application (a special NodeJS wrapper around Chromium)
- Managing Application lifecycles (e.g. activating on Mac, App Ready, App windows closed) that are closely tied to system events
- 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 Appmenu.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:
In this structure:
build
will represent transpiled electron files ready to be bundled as a standalone system installerdist
will hold both Electron and Create React app production fileselectron
is a container for all the Electron-related codepublic
andsrc
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
:
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:
- Defines the
createWindow
function that will set-up the App - 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 (henceBROWSER=none
)tsc --project electron/tsconfig.json
that will transpile Electron files and put them inbuild
directoryelectron .
that will instruct Electron to look for the config in the current directory (project directory) and bootstrap the app based on themain
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:
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:
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:
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:
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 😉