React + Electron + Typescript — a dev experience (part 1)

Bartek Polanczyk
8 min readJun 30, 2020

--

Nearly 1,5 years ago I had an amazing opportunity to lead a workshop during GDG Warsaw DevFest. The goal was to build a multi-platform (Desktop + Web) app using commonly available tools:

  • Create React App to bootstrap web part of the app
  • Typescript (everywhere!) as a language of choice
  • Common linting tools (Stylelint, ESLint) to keep the code clean
  • Electron to build a native desktop experience

The initial goal took much more than I expected, though — everyone got so interested in Typescript we never made it to Electron part! It took me another couple of evenings to finish it throughout next year.

As a result, I was able to create a unified code experience — a fully working dev + production mode app that is fully utilizing the tools above to streamline developing such apps. No ejecting needed, no tedious configurations, most of the code works on both Unix and Windows dev machines.

In the first part, we’ll focus on creating a conventional web app using Create React App, Material UI, and adding linters to the project.

Just give me the code!

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

Front end — Create React App and Typescript

The first step is to create a front end part of the app — we’ll be using Create React App with Typescript: https://github.com/facebook/create-react-app

There are two ways of achieving that:

  1. Creating a standard JS app and then converting it to Typescript
  2. Creating an app using Typescript template

We’ll be modifying the tsconfig.json files nevertheless (it’s the main config file for Typescript) so it doesn’t matter which one you choose. If you choose the first method, you’ll need to manually install a few packages though, so we’ll choose a simpler method:

npx create-react-app gdg-react-workshop --template typescript

All the Electron files we’ll add in the next steps will be seamlessly integrated into the structure created by the CRA so we don’t have to worry about rewriting it later. For the time being, we should be also OK with the default tsconfig.json that Create React App created from the template.

Create React App already offers live reloading and proper dev/build pipelines — the goal is to leverage as much of it as possible so it will stay untouched for the full build. You can find more on CRA here: https://create-react-app.dev/docs/getting-started

It’s time to add some components to the mix!

UI framework — Material UI

I chose Material UI as a UI framework for the application — it is well-opinionated and offers a standardized experience that should suit both web and desktop user needs: https://material-ui.com/

Material installation is rather straightforward — we’ll need the core framework and icons:

yarn add @material-ui/core @material-ui/icons

We’ll also need Typescript types to have proper code completion and compilation for Material UI components:

yarn add --dev @types/material-ui

External types (usually residing in a huge @types mono-repo) are a way of telling the Typescript compiler how do the libraries our project consume look like — they provide a set of classes, interfaces and other Typescript-recognizable structures that live in parallel to real code and allow for things such as:

  • Code completion (you don’t need to use Typescript directly to improve code completion in your editor that way!)
  • Leveraging type guards in the code (e.g. on prop-level in the components)
  • Building complex types based on internal library types (e.g. describing component properties in Typescript) — we’ll get to that in a moment

As a rule of thumb — whenever you install a new library for your Typescript project (that does not supply its types — it happens more often than you would think), try to find the types for the library in the monorepo, e.g. by searching for them at https://microsoft.github.io/TypeSearch/

If the types in the monorepo are obsolete, you should get a proper message during their installation, e.g. (when installing @types/electron ):

warning @types/electron@1.6.10: This is a stub types definition for electron (https://github.com/electron/electron). electron provides its own type definitions, so you don't need @types/electron installed!

Time to create a few components! You’ll find a fully working code in the repository, let’s focus on key components only. First, we need to customize the main App.tsx file so that it’ll provide Material UI context and other initialization code:

Note especially:

  • Theme initialization — we use a simple way of switching between dark/light mode (they’re enabled between 10 pm and 8 am)
  • A simple interface — AppProps — this is a definition of the components’ properties. width property comes from the Higher Order Component — withWidth() and has to be defined for the Typescript to support it properly within the component (with type-checking)
  • A simple class component that renders HashRouter, Material UI provider — MuiThemeProvider, and CSS baseline/reset ( CssBaseline ) — generic app elements.

Note that I used a class component, but it’d be probably better to use hooks, however, I prefer to follow the rule of not rewriting the code that works well — it was written before hooks became the biggest “thing” in React world 😃

Adding Image Service

Most of the UI code in the workshop is created purely to show that browser APIs work seamlessly in Electron environment as well. I chose to create an image browser that allows us to store a list of images in the storage, regardless of whether the storage comes from a standard browser or electron BrowserWindow (which in reality is almost the same as headless Chromium browser).

To read and store the images we’ll need a common code — a service that will interact with the browser API and will shield the component code from low-level file operations:

As we’re using Typescript, we can leverage types — to standardize the way the service interacts with the code, it operates on a model calledStoredImage . The definition of the model specifies what should every instance of it include:

To be able to load images into our app, we also need a way of telling Typescript that it should interpret image extensions as TS modules — we’ll create a custom type mapping.

As all of the code in /src directory is compiled by Typescript by default, we don’t have to worry about specifying paths to our types (it’s something that usually haunted TypeScript devs in its beginnings), we just need to create our types specifications, e.g. in src/types/images.d.ts :

That way every GIF and PNG file we add we’ll be properly supported without compilation errors (we don’t have to worry about their internal representation, we just want TypeScript to allow us to add such file formats to our code). This way of defining modules allows for quick prototyping whenever you want to use a JS library that does not have any type declarations available — you only need to tell TypeScript that it’s a compatible module. It’ll allow you to import it to your code (obviously without full code completion, though).

Improving tsconfig.json

tsconfig.json is the main configuration that TypeScript uses when it’s compiling your code — it defines the way the files are both interpreted and compiled and also the environment available in TS files. You can find more in official docs here: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html

For our purposes we want to create a config that will work with both Electron and React and also will be restrictive enough so that our code is as clean as possible:

The most important parts of this config are:

  • module: esnext — Typescript will emit ESNext modules ( import/export style) that will be then transpiled by Webpack
  • lib section, including DOM and ES6 — these modules will be available as native/globals in your code (this allows for using e.g. File API in ImagesService)
  • noImplicitX —these options force writing code in a strongly-typed manner, without leaving as much implicit code as possible
  • strict: true — this keyword turns on a set of restrictive checks (e.g. for undefined values) and enforces developer to write code that is not ambiguous
  • exclude and include are lists of patterns that will be respectively excluded and included in the build

Now it’s a good moment to add a linter!

Linting your code — unified ESLint config

ESLint is an all-in-one tool to control and improve your code quality and it supports TypeScript as well! Not a long time ago Palantir, a company that was a heavy supporter of TypeScript decided to let go of their own linter — TSLint — that was specific for linting TypeScript files in favor of providing a similar set of tools in ESLint. Thanks to that decision (and a lot of support for Typescript-ESLint maintainers), now you can use one tool to Lint them all!

ESLint config usually resides in .eslintrc.json or .eslintrc.js file and consists of a set of rules and configuration options. The config below prepares a standardized Typescript/React config that should be feasible for any React + Typescript app:

Before we can use it, we need to install required toolset, though:

yarn add --dev @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-react

We can skip installing eslint itself as it should be provided under the hood by Create React App scripts.

As I decided to use plain CSS to customize the looks of my components, I also wanted to have a way of style-checking CSS files. The tool that allows that is called Stylelint. Its config resides in .stylelintrc.json (you can find a full file at https://github.com/SzybkiSasza/gdg-react-workshop/blob/master/.stylelintrc.json):

To be able to use Stylelint in the project, we need to add related libraries:

yarn add --dev stylelint stylelint-config-standard stylelint-order

Such config will ensure that the properties are used in a standardized way and also that their order is preserved. Now we need to add our toolset to NPM scripts!

Adding ESLint and Stylelint to scripts

The tools are installed and therefore — we can use it in IDEs providing that correct plugins are installed (check your editor manual/guidelines on how to enable support for ESLint and Stylelint). Although, if we want to use these tools in the CI/connect to GIT hooks or just use in the CLI, we still need to create NPM scripts for them (other scripts were skipped for brevity):

"scripts": {
"lint": "eslint ./src --ext \".js,.jsx,.ts,.tsx\" && stylelint \"./src/**/*.css\"",
"lint:fix": "eslint ./src --ext \".js,.jsx,.ts,.tsx\" --fix && stylelint \"./src/**/*.css\" --fix"
}

First of the scripts runs a standard check (checks your files for breaking issues) whereas the second one is a magical one — it can fix all the issues in your code that are defined as “fixable” — e.g. indentation, commas, quoting, etc.

This is probably the biggest power of linters and linting configs — a well-defined set of rules allows for automatically fixing the code that violates them!

You can even fix the files from within the IDE, e.g. (for WebStorm):

To sum up…

Now we’re ready to add Electron to our code — we created an app using Create React App with Typescript and added needed configuration. We also made sure that our code is standardized using ESLint and Stylelint.

If you liked this part— like and share and stay tuned for the next part — Adding Electron and running the code as a native desktop app!

--

--

Bartek Polanczyk

Senior Engineer @sumologic, traveler, diver, homegrown musician