Create Your First React Electron Desktop App With TypeScript

In a previous piece, I went over the steps of creating desktop application software using Electron. This piece will start off by cloning the repo and extending it to support TypeScript so that we get type-checking capabilities while developing our desktop app.

TypeScript can help improve your app quality by informing you of type errors in your code, so it’s a good idea to start integrating this into your development flow if you haven’t already.

With that said, this is not a continuation tutorial, but we will be using the repo to extend it so that users like you and I can start taking advantage of TypeScript features when developing desktop apps.

And without further ado, let’s get started! (Note: If you want to have a copy of the resulting repo that we will be building, visit this link)

The first thing we are going to do is to clone the repo.

After it’s done, go into the directory and install the dependencies using the CLI:

npm install

Once it’s done installing the app, lets make sure that we have a working project by starting it up in dev mode:

npm start

If it was successful, you should see this window:

This is image title

That started up our live hot reloadable web server for our React app.

Now go ahead and run Electron:

npm run electron

If that was successful, you should then see this window:

This is image title

Great! Now that we know we have a working app, let’s continue with installing TypeScript into the project:

npm i -D typescript

(Note: -D is just an alias for --save-dev)

We’re going to install ESLint next. You might be wondering why I’m even bothering with ESLint since it is mainly in concern with linting JavaScript.

The team behind TSLint made an announcement earlier this year announcing their plans moving forward and decided that TSLint will become deprecated in favor of ESLint. As a result, tools were eventually developed onward that allow developers to use ESLint and TypeScript together.

@typescript-eslint/parser is a parser that turns our source code into an Abstract Syntax Tree (AST) that enables ESLint to be used with TypeScript by utilizing the TypeScript compiler. You can read about it on GitHub for more information.

We will also need to install @typescript-eslint/eslint-plugin.

I’m going to list the packages that I regularly use in my React projects. You don’t have to install all of them, buteslint and the bottom five of this list are what you’ll most definitely want to use in your projects:

  • eslint
  • eslint-config-airbnb
  • eslint-config-prettier
  • eslint-plugin-import
  • eslint-plugin-jsx-a11y
  • eslint-plugin-prettier
  • eslint-plugin-react
  • eslint-plugin-react-hooks
  • @typescript-eslint/parser
  • @typescript-eslint/eslint-plugin

So let’s go ahead and install eslint and all of the others:

npm install -D eslint eslint-config-airbnb eslint-config-prettier eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-prettier eslint-plugin-react eslint-plugin-react-hooks @typescript-eslint/parser @typescript-eslint/eslint-plugin

Let’s also not forget about typescript itself:

npm install -D typescript

Next, we’re going to create a .eslintrc.js file in our root directory. Here’s my .eslintrc.js:

module.exports = {
  parser: '@typescript-eslint/parser',
  parserOptions: {
    project: './tsconfig.json',
    ecmaFeatures: {
      jsx: true,
    },
  },
  env: {
    browser: true,
    jest: true,
  },
  extends: [
    'airbnb',
    'prettier',
    'prettier/react',
    'prettier/@typescript-eslint',
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended',
  ],
  plugins: ['@typescript-eslint', 'react-hooks', 'prettier'],
  rules: {
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/indent': 'off',
    '@typescript-eslint/explicit-member-accessibility': 'off',
    '@typescript-eslint/member-delimiter-style': 'off',
    '@typescript-eslint/no-use-before-define': 'off',
    '@typescript-eslint/no-explicit-any': 'off',
    '@typescript-eslint/camelcase': 'off',
    'arrow-parens': [2, 'always'],
    'arrow-body-style': 0,
    'consistent-return': 0,
    'css-modules/no-unused-class': 'off',
    camelcase: 0,
    'class-methods-use-this': 0,
    'comma-dangle': 0,
    'dot-notation': 0,
    eqeqeq: 0,
    'flowtype/no-types-missing-file-annotation': 0,
    'func-names': 'off',
    'import/prefer-default-export': 0,
    'import/no-extraneous-dependencies': 'off',
    'import/newline-after-import': 'off',
    'import/first': 'off',
    'import/no-extensions': 'off',
    'import/extensions': 'off',
    'import/no-unresolved': 'off',
    'import/no-useless-path-segments': 0,
    'import/no-absolute-path': 'off',
    'jsx-a11y/html-has-lang': 0,
    'jsx-a11y/alt-text': 0,
    'jsx-a11y/anchor-is-valid': 'off',
    'jsx-a11y/click-events-have-key-events': 'off',
    'jsx-a11y/href-no-hash': 0,
    'jsx-a11y/no-static-element-interactions': 0,
    'jsx-a11y/no-noninteractive-element-interactions': 0,
    'jsx-a11y/no-autofocus': 0,
    'jsx-a11y/label-has-associated-control': 0,
    'jsx-a11y/label-has-for': 0,
    'jsx-quotes': ['error', 'prefer-double'],
    'jsx-a11y/media-has-caption': 0,
    'jsx-a11y/anchor-has-content': 0,
    'linebreak-style': 0,
    'max-len': 0,
    'no-alert': 0,
    'no-case-declarations': 0,
    'no-underscore-dangle': 'off',
    'no-useless-escape': 'off',
    'no-trailing-spaces': 0,
    'no-multi-assign': 'off',
    'no-nested-ternary': 'off',
    'no-lonely-if': 'off',
    'no-plusplus': 'off',
    'no-loop-func': 'off',
    'no-unused-expressions': 0,
    'no-unused-vars': 1,
    'no-confusing-arrow': 0,
    'no-use-before-define': 0,
    'no-console': 0,
    'no-return-assign': 0,
    'no-restricted-properties': 0,
    'no-param-reassign': 0,
    'no-shadow': 0,
    'no-prototype-builtins': 0,
    'no-multiple-empty-lines': 0,
    'no-else-return': 0,
    'object-curly-spacing': ['error', 'always'],
    'object-property-newline': 0,
    'one-var': 0,
    'one-var-declaration-per-line': 0,
    'prettier/prettier': 0,
    'padded-blocks': 0,
    'prefer-template': 0,
    'prefer-destructuring': 0,
    quotes: 2,
    'react-hooks/exhaustive-deps': 'warn',
    'react-hooks/rules-of-hooks': 'error',
    'react/no-multi-comp': 0,
    'react/jsx-wrap-multilines': 0,
    'react/default-props-match-prop-types': 'off',
    'react/no-find-dom-node': 'off',
    'react/destructuring-assignment': 'off',
    'react/jsx-no-bind': 'off',
    'react/jsx-filename-extension': [
      'error',
      {
        extensions: ['.js', '.jsx', '.ts', '.tsx'],
      },
    ],
    'react/react-in-jsx-scope': 0,
    'react/prop-types': 0,
    'react/forbid-prop-types': 0,
    'react/no-children-prop': 0,
    'react/no-array-index-key': 0,
    'react/prefer-stateless-function': 'off',
    'react/sort-comp': 0,
    'react/no-unescaped-entities': 0,
    'react/jsx-no-bind': 0,
    'react/no-unused-state': 1,
    'react/no-unused-prop-types': 0,
    'react/jsx-pascal-case': 0,
    'react/no-danger': 0,
    'react/require-default-props': 0,
    'react/jsx-curly-spacing': 0,
    'react/jsx-max-props-per-line': 1,
    'space-in-parens': ['error', 'never'],
    'spaced-comment': 0,
    'space-infix-ops': 0,
    'space-unary-ops': 0,
    'space-before-function-paren': 0,
  },
  settings: {
    'import/resolver': {
      node: {
        moduleDirectory: ['node_modules', 'src'],
      },
    },
  },
}

eslintrc.js

Now when we implement TypeScript into an Electron project, it gets a little tricky. TypeScript is a typed superset of JavaScript that compiles code to plain JavaScript, which is what we want. But there might actually be an issue on this when building apps in Electron that we might not have been aware of at first glance, especially if we just started using Electron.

The problem is that there are actually two types of processes that run in Electron. One is called the main process and the other is the renderer process. When Electron creates web pages, they’re created as renderer processes (which are essentially living in a browser environment). Electron can create and run multiple renderer processes at the same time, but ultimately there can only be one main process.

Since renderer processes are web pages, they’re blocked from calling native GUI APIs because it would be a huge security concern to allow them to manage GUI resources. Electron enables a one-way communication tunnel between the renderer and the main process by utilizing ipcMain, ipcRenderer, or remote.

Because of this restriction, we must split the directories in such a way that we develop code for the main process separately apart from the renderer process, so that we have TypeScript compile them separately. This is so we don’t create problems in the software from compiling together their code.

Let’s look at our directory structure and see what we’ve got:

This is image title

It looks like we have start.js, which is the main process, living in the same directory as the code of the renderer process (App.js, index.js, index.css, etc). So we have to separate them to something like this:

Note: I renamed the files in the screenshot so that they are TypeScript files.

This is image title

This is a good start. However, when we configure the typescript config file we have to specify a glob that TypeScript will use to include in all the files that it matches in the compilation, including where to output them to. We’re still stuck at the previous issue, so what we’re going to do is to make the current root directory to be the parent directory, which will hold the mainand renderer process code.

We’re also going to make both of them be independent repos so that we can gain the benefits of npm installing packages that only need to exposed to a specific process and vice versa. This will help give us an easier time debugging in the future from having our directories more abstracted and organized.

What we’re going to do is to move everything except the main directory to the renderer directory. The reason we do this is because this project was bootstrapped by create-react-app, which is essentially already an environment inside a renderer process:

This is image title

Now that we’ve got the renderer repo out of the way, let’s make the main process into its own repo:

# step into the main directory
cd main
# initialize npm
npm init

Just press Enter through everything. Now open up the package.json and you should see a nearly empty package.json file:

{
  "name": "main",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

What we’re going to need to change here is the "main" part, not because it’s not a TypeScript file, but because this is the Electron file we are going to be putting in our output directory when we run the build command later. When we build our app, we’re going to initiate it inside the renderer directory so we need a clearer name:

{
  "name": "main",
  "version": "1.0.0",
  "description": "",
  "main": "./src/electron.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

You might be confused as to why we didn’t write the name ending with a TypeScript extension like .ts. This is because we’re going to create a electron.ts file which we will make TypeScript transpile it to .js in the same directory. When we run the build command in the renderer directory later, we are going to programmatically copy this file and send it to the renderer’s output directory, which will be build.

In order to get TypeScript to compile this file, we’re going to install TypeScript in the main repo:

npm install -D typescript

Then we’re going to create a tsconfig.json in its root directory:

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve"
  },
  "include": ["src"]
}

We’re going to treat this as a typical repo for developing as we don’t want any unnecessary confusions going back and forth switching in between, so we’ll create a src directory and move the start.ts file right into it.

This start.ts file will be the electron.ts file that will be compiled right into electron.js.

Also, don’t forget to install electron:

npm install electron && npm install -D @types/electron electron-is-dev

In addition, we’re going to install the nodemon package so that we acquire auto restartcapabilities when we combine it with electron-reload (electron-reload is used to restart the main process when we make changes to it):

npm install --save-dev nodemon electron-reload

Next, we’re going to add the start command to the scripts section:

{
  "name": "main",
  "version": "1.0.0",
  "description": "",
  "main": "./src/electron.js",
  "scripts": {
    "start": "cross-env NODE_ENV=dev nodemon --exec \"electron src/electron.js\" && tsc ./src/electron.ts -w"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "electron": "^6.0.12"
  },
  "devDependencies": {
    "@types/electron": "^1.6.10",
    "concurrently": "^5.0.0",
    "cross-env": "^6.0.3",
    "electron-is-dev": "^1.1.0",
    "electron-reload": "^1.5.0",
    "nodemon": "^1.19.3",
    "typescript": "^3.6.4"
  }
}

And this is our electron.ts file:

import { app, BrowserWindow } from 'electron'
import * as path from 'path'
import * as isDev from 'electron-is-dev'
import 'electron-reload'
let mainWindow
function createWindow() {
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true,
    },
  })
  mainWindow.loadURL(
    isDev
      ? 'http://localhost:3000'
      : `file://${path.join(__dirname, '../build/index.html')}`,
  )
  mainWindow.on('closed', () => {
    mainWindow = null
  })
}
app.on('ready', createWindow)
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})
app.on('activate', () => {
  if (mainWindow === null) {
    createWindow()
  }
})

Great! Now when we run npm start, our main process should run successfully, in addition to automatically re-compiling electron.ts to electron.js on changes:

This is image title

Let’s move back into the renderer directory because there are a couple of things we still need to do.

# move back out to the parent directory
cd ..
# move into the renderer directory
cd renderer

Note: If you’re missing a tsconfig.json file, create it:

{
  "compilerOptions": {
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "lib": ["dom", "dom.iterable", "esnext"],
    "module": "esnext",
    "moduleResolution": "node",
    "noEmit": true,
    "skipLibCheck": true,
    "strict": false,
    "target": "es5",
    "resolveJsonModule": true
  },
  "include": ["src"]
}

If all goes well, we should now have two working processes!

Go into your renderer process directory and run npm start where you should see a working and running server as expected:

This is image title

And finally, open up another terminal, go into your main process directory and run npm startas well. It should be working as well:

This is image title

Hurray! We finally did it! We can now start almost developing! Wait, what?

Yes, that’s right. We’re not completely done yet. Have you noticed that when you make changes to the main process code, Electron is not reloading? We’re going to need the wait-on package to call the shots on when to execute the electron.js file. This perfectly solves our problem since it waits until HTTP requests return a 200 code and then it will continue to execute the script when the app is ready to continue.

We’re also going to use concurrently so that we can run our commands at the same time since they can be run individually:

{
  "name": "main",
  "version": "1.0.0",
  "description": "",
  "main": "./src/electron.js",
  "scripts": {
    "start": "concurrently \"tsc ./src/electron.ts -w\" \"cross-env NODE_ENV=dev nodemon --exec \"\"wait-on http://localhost:3000 && electron src/electron.js\"\""
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "electron": "^6.0.12"
  },
  "devDependencies": {
    "@types/electron": "^1.6.10",
    "concurrently": "^5.0.0",
    "cross-env": "^6.0.3",
    "electron-is-dev": "^1.1.0",
    "electron-reload": "^1.5.0",
    "nodemon": "^1.19.3",
    "typescript": "^3.6.4",
    "wait-on": "^3.3.0"
  }
}

Once you reach this point, you can then begin developing your app code however you like. Remember, you’re able to develop the main process separately from your renderer process, but they will be packaged together when you package them with electron-builder.

Conclusion

And that concludes the end of this piece. I hope you found this to be valuable and that it helped you gain a little more understanding of how you can integrate TypeScript into other projects. Look for more in the future!

#reactjs #javascript #Electron #TypeScript

Create Your First  React Electron Desktop App  With TypeScript
15.15 GEEK