React, Typescript, Electron, all steps to start
Tags: react, typescript, electron
Published: 2020-03-29 14:23:18

The guides and issues are scattered, I summarized all together to start the journey.

Two computers for testing:
1. Mac - MacBook Pro (Retina, 13-inch, Early 2015): macOS High Sierra (Version 10.13.6), English.
2. Win - ThinkPad T430: Windows 10 professional, Chinese.

Setup steps:
1. Install node.js from https://nodejs.org/
2. Mac: Terminal, Win: Node.js command prompt. input command 'node -v' or 'node --version' to show the version of node.js and make sure the installation is successful. Installations below are global(-g) visible. If you want to limit to a specific project, add '--save-dev' before the package.
3. 'npm install -g create-react-app' to install React (https://reactjs.org/).
4. 'npm install -g typescript' to install TypeScript (https://www.typescriptlang.org/). Step 3 and 4 together command 'npx create-react-app my-app --template typescript'.
5. 'npm install -g electron' to install Electron (https://www.electronjs.org/).
6. 'npm install -g electron-builder' for packaging Electron application.
7. 'npm install -g concurrently' for concurrent commands support.
8. 'npm install -g wait-on' for step by step commands support.
On Mac, you may be failed because of 'Missing write access permission'. Please access to https://flaviocopes.com/npm-fix-missing-write-access-error/ to fix it.

Configuration steps:
1. Mac starts from the user's home folder. Mac & Win both use 'cd' to change directory.
2. 'npx create-react-app my-app --template typescript' create an application named 'my-app' (change it to your project name) with typescript support. Use 'cd my-app' to the project folder. Setting up ESLint on VS Code with Airbnb JavaScript Style Guide: https://travishorn.com/setting-up-eslint-on-vs-code-with-airbnb-javascript-style-guide-6eb78a535ba6
3. 'npm start' will launch the project and open a browser window to load URL 'http://localhost:3000/'. It works then the React works.
4. Create an 'electron.ts' under the 'public' folder. Please check the sample codes below.
5. Create a 'preload.js' under the 'public' folder. Please check the sample codes below.
6. Edit 'src/App.tsx', add codes to communicate with Electron main process. Please check the sample codes (App.tsx, IBridge.ts, IAppData.ts, ElectronBrideg.ts) below.
7. Edit 'package.json', add lines:
```
"main": "public/electron.ts", -- Electron initialization script
"homepage": "./",  -- set it to fix possible ‘Failed to load resource’ errors
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "electron": "concurrently \"set BROWSER=none&&npm start\" \"wait-on http://localhost:3000&&set ELECTRON_ENV=development&&electron .\"", -- Win
    "electron": "concurrently \"export BROWSER=none&&npm start\" \"wait-on http://localhost:3000&&export ELECTRON_ENV=development&&electron .\"", -- Mac
    -- set or export ELECTRON_ENV to pass production environment variable
    "package": "npm run build&&electron-builder build"
  },
  "build": {
    "appId": "***",
    "copyright": "***",
    "productName": "***",
    "files": [
      "build/",
      "public/electron.ts",
      "!node_modules/"
    ],
    "mac": {
      "icon": "public/logo512.png",
      "category": "public.app-category.utilities"
      -- Need to provide signing details
      -- https://www.electron.build/configuration/mac
    },
    "win": {
      "icon": "public/logo512.png",
      "target": [
        "nsis",
        "msi"
      ]
    }
  }
```
-- 'npm install' to fix possilbe dependencies or links issues.
-- Remove "react-scripts": "3.4.1" to avoid react-builder tricky error.
-- Add <meta http-equiv="Content-Security-Policy" content="script-src 'self' https://*.any.com/"> to improve security when load URLs (not file://).
8. use 'npm run electron' in development mode. Please run 'npm run build' once before; use 'npm run package' to build the application. You can press 'Ctrl + C' to exit node.js process and run a new command.

Sample codes:
1. electron.ts:
```
// Modules to control application life and create native browser window
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')
const isDev = process.env.ELECTRON_ENV?.trim() == 'development'

function createWindow() {
    // Create the browser window.
    const mainWindow = new BrowserWindow({
        show: false,
        webPreferences: {
            contextIsolation: true,
            preload: path.join(app.getAppPath(), './build/preload.js')
        }
    })

    // Show the window after its ready
    mainWindow.once('ready-to-show', () => {
        // Show the window
        mainWindow.show()

        // Maximize the window
        mainWindow.maximize()

        // Not allowed to resize
        // mainWindow.resizable = false
    })

    // Hide the application menu
    mainWindow.setMenuBarVisibility(false)

    // and load the app.
    if (isDev)
        mainWindow.loadURL('http://localhost:3000/')
    else
        mainWindow.loadFile(`${path.join(app.getAppPath(), './build/index.html')}`)

    // Open the DevTools.
    // Use 'Ctrl + Shift + I' instead
    // mainWindow.webContents.openDevTools()
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow)

// Quit when all windows are closed.
app.on('window-all-closed', function () {
    // On macOS it is common for applications and their menu bar
    // to stay active until the user quits explicitly with Cmd + Q
    if (process.platform !== 'darwin') app.quit()
})

app.on('activate', function () {
    // On macOS it's common to re-create a window in the app when the
    // dock icon is clicked and there are no other windows open.
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
})

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
ipcMain.on('app', (event, arg) => {
    event.reply('app-reply', { name: app.name, version: app.getVersion(), path: app.getAppPath() })
})
```
2. preload.js:
```
const {
    contextBridge,
    ipcRenderer
} = require("electron")

// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld(
    "appRuntime", {
        send: (channel, data = null) => {
            ipcRenderer.send(channel, data);
        },
        subscribe: (channel, listener) => {
            const subscription = (event, ...args) => listener(...args);
            ipcRenderer.on(channel, subscription);

            return () => {
                ipcRenderer.removeListener(channel, subscription);
            }
        }
    }
)
```
3. App.tsx:
```
import React, {useState} from 'react'
import logo from './logo.svg'
import 'antd/dist/antd.css'
import './App.css'

import appRuntime from "./etsoo/bridges/ElectronBridge"
import IAppData from "./etsoo/bridges/IAppData";

function App() {
  const [appData, setAppData] = useState('Loading...')

  if(appRuntime != null) {
    appRuntime.subscribe('app-reply', (data:IAppData) => {
      setAppData(data.name + ': ' + data.version)
    })
    appRuntime.send('app')
  }

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <p>
          App: {appData}
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  )
}

export default App
```
4. etsoo/bridges/IBridge.ts:
```
/**
 * IBridge unsubscribe type
 */
type IBridgeUnsubscribe = () => void

/**
 * IBridge subscribe listener type
 */
type IBridgeListener = (...args: any[]) => void

/**
 * window.external calls bridge interface
 */
export interface IBridge {
    /**
     * Send data to the host process with an unique channel
     * @param channel an unique channel
     * @param data Data to send
     */
    send(channel: string, data?: any):void

    /**
     * Subscribe to the host process
     * @param channel an unique channel
     * @param listener callback listener
     */
    subscribe(channel: string, listener: IBridgeListener):IBridgeUnsubscribe
}
```
5. etsoo/bridges/IAppData.ts:
```
/**
 * Bridge App Data interface
 */
export default interface IAppData {
    /**
     * Application name
     */
    name: string,

    /**
     * Application version
     */
    version: string,

    /**
     * Application path
     */
    path: string
}
```
6. etsoo/bridges/ElectronBridge.ts:
```
import { IBridge } from './IBridge'

/**
 * Electron bridge class
 * copes with preload.js, contextBridge.exposeInMainWorld
 * BrowserWindow.webPreferences, contextIsolation: true
 */
const appRuntime = (window as any).appRuntime as IBridge
export default appRuntime
```