The Bridge Between Web and Desktop
You no longer need years of C++ or C# expertise to build a world-class desktop application. Apps like Discord, Slack, and VS Code have proven that web technologies can dominate the desktop. By combining Electron.js with React and TypeScript, you leverage a massive ecosystem of libraries while maintaining the type safety required for complex software.
After shipping several production tools with this stack, I’ve found it to be the “sweet spot” for developer velocity. The stability is impressive if you respect the boundary between the UI and the system. Let’s move past the theory and build a setup that is ready for production.
Zero to Hello World in 5 Minutes
Manual configuration is a recipe for frustration. For a modern workflow, I recommend electron-vite. It delivers lightning-fast Hot Module Replacement (HMR)—often refreshing in under 100ms—and handles the TypeScript compilation for you.
Open your terminal and run:
npm create @electron-vite/project@latest my-electron-app -- --template react-ts
Navigate into your project and fire up the development environment:
cd my-electron-app
npm install
npm run dev
A window should pop up instantly. Notice the folder structure: main, renderer, and preload. This isn’t just organization; it is a security requirement. Keeping these layers separate prevents a vulnerability in your UI from giving an attacker full control over the user’s computer.
How It Works Under the Hood
To build anything significant, you have to understand Electron’s multi-process model. It functions more like a cluster of programs than a single app.
1. The Main Process
This is the “boss” process. It runs in Node.js, manages your app’s lifecycle, and holds the keys to the operating system. It can trigger native dialogs, manage system trays, and touch the file system directly.
2. The Renderer Process
This is essentially a specialized Chrome tab. Your React code lives here. By default, it is sandboxed. It cannot read local files or execute shell commands. This restriction keeps your users safe.
3. The Preload Script
Think of this as a secure airlock. It allows the Main and Renderer processes to talk without exposing dangerous Node.js internals to the frontend. You’ll use the contextBridge to pass specific, safe functions to your React components.
Here is a clean setup for src/preload/index.ts:
import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('electronAPI', {
saveFile: (content: string) => ipcRenderer.invoke('dialog:saveFile', content),
onUpdateStatus: (callback: (status: string) => void) =>
ipcRenderer.on('status-changed', (_event, value) => callback(value))
})
Real-World IPC: Saving Files
Inter-Process Communication (IPC) is the heart of Electron development. Let’s implement a feature where the user saves a text file to their local drive.
Step 1: The Main Process Handler
In src/main/index.ts, we listen for the request and open a native save dialog:
import { ipcMain, dialog } from 'electron'
import fs from 'fs'
ipcMain.handle('dialog:saveFile', async (event, content: string) => {
const { filePath } = await dialog.showSaveDialog({
title: 'Save your work',
defaultPath: 'note.txt'
})
if (filePath) {
fs.writeFileSync(filePath, content, 'utf-8')
return true
}
return false
})
Step 2: The React Implementation
First, extend the Window interface so TypeScript doesn’t complain. Create src/renderer/src/env.d.ts:
interface Window {
electronAPI: {
saveFile: (content: string) => Promise<boolean>
}
}
Now, trigger the save from your React component:
import { useState } from 'react'
function App() {
const [text, setText] = useState('')
const handleSave = async () => {
const success = await window.electronAPI.saveFile(text)
if (success) alert('File saved successfully!')
}
return (
<div className="p-4">
<textarea
className="w-full border"
onChange={(e) => setText(e.target.value)}
/>
<button onClick={handleSave}>Save to Desktop</button>
</div>
)
}
Packaging for Distribution
A great app is useless if you can’t ship it. I use electron-builder because it simplifies the headache of code signing and auto-updates. It can generate .exe, .dmg, and .AppImage files from a single config.
Your package.json configuration should look like this:
"build": {
"appId": "com.yourname.myapp",
"productName": "My Electron App",
"win": { "target": ["nsis"] },
"mac": { "target": ["dmg"] }
}
Run npm run build:win or npm run build:mac to generate your installer. The process usually takes about 1-2 minutes depending on your assets.
Hard-Won Advice for Developers
- Mind the Payload: A “Hello World” Electron app starts at roughly 80MB. This is because you are shipping a full copy of Chromium. Keep your
node_moduleslean to avoid bloating this further. - Native UX: Web scrollbars and text selection are dead giveaways that an app isn’t native. Use
user-select: none;in your CSS for UI elements. Disable image dragging with-webkit-user-drag: none;to make it feel like a real desktop app. - Security is Non-Negotiable: Never set
nodeIntegration: true. It’s a massive security hole. Always use the preload script to bridge the gap. - Async Everything: Never use synchronous file system calls (
fs.readFileSync) in the Main process if it can be avoided. It will freeze your entire UI.
Electron is an incredibly rewarding framework. It lets you reach users on their desktops while using the skills you already have. Start with a small utility, nail the IPC communication, and you’ll be ready to ship professional software in no time.

