DEV Community

Garry Xiao
Garry Xiao

Posted on • Edited on

1 1

React, Typescript, Electron, all steps to start

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"
      ]
    }
  }
Enter fullscreen mode Exit fullscreen mode

-- 'npm install' to fix possilbe dependencies or links issues.
-- Remove "react-scripts": "3.4.1" to avoid react-builder tricky error.
-- Add to improve security when load URLs (not file://).

  1. 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() })
})
Enter fullscreen mode Exit fullscreen mode
  1. 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);
            }
        }
    }
)
Enter fullscreen mode Exit fullscreen mode
  1. 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
Enter fullscreen mode Exit fullscreen mode
  1. 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
}
Enter fullscreen mode Exit fullscreen mode
  1. etsoo/bridges/IAppData.ts:
/**
 * Bridge App Data interface
 */
export default interface IAppData {
    /**
     * Application name
     */
    name: string,

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

    /**
     * Application path
     */
    path: string
}
Enter fullscreen mode Exit fullscreen mode
  1. 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
Enter fullscreen mode Exit fullscreen mode

Qodo Takeover

Introducing Qodo Gen 1.0: Transform Your Workflow with Agentic AI

While many AI coding tools operate as simple command-response systems, Qodo Gen 1.0 represents the next generation: autonomous, multi-step problem-solving agents that work alongside you.

Read full post

Top comments (0)

Qodo Takeover

Introducing Qodo Gen 1.0: Transform Your Workflow with Agentic AI

Rather than just generating snippets, our agents understand your entire project context, can make decisions, use tools, and carry out tasks autonomously.

Read full post

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay