When to Use
Load this skill when:
-
Building cross-platform desktop applications
-
Working with Electron's main and renderer processes
-
Implementing IPC (Inter-Process Communication)
-
Integrating native OS features (menus, notifications, file system)
-
Setting up Electron with React, Vue, or other frameworks
-
Configuring auto-updates and app distribution
Critical Patterns
Pattern 1: Project Structure
src/ ├── main/ # Main process (Node.js) │ ├── index.ts # Entry point │ ├── ipc/ # IPC handlers │ │ ├── handlers.ts │ │ └── channels.ts # Type-safe channel names │ ├── services/ # Native services │ │ ├── store.ts # electron-store │ │ └── updater.ts # auto-updater │ └── windows/ # Window management │ └── main-window.ts ├── renderer/ # Renderer process (browser) │ ├── src/ │ │ ├── App.tsx │ │ ├── components/ │ │ └── hooks/ │ │ └── useIPC.ts # IPC hooks │ └── index.html ├── preload/ # Preload scripts │ └── index.ts # Expose safe APIs └── shared/ # Shared types └── types.ts
Pattern 2: Secure IPC Communication
Always use contextBridge for secure communication:
// preload/index.ts import { contextBridge, ipcRenderer } from 'electron'; import type { IpcChannels } from '../shared/types';
// Type-safe exposed API const electronAPI = { // One-way: renderer -> main send: <T extends keyof IpcChannels>( channel: T, data: IpcChannels[T]['request'] ) => { ipcRenderer.send(channel, data); },
// Two-way: renderer -> main -> renderer invoke: <T extends keyof IpcChannels>( channel: T, data: IpcChannels[T]['request'] ): Promise<IpcChannels[T]['response']> => { return ipcRenderer.invoke(channel, data); },
// Listen: main -> renderer on: <T extends keyof IpcChannels>( channel: T, callback: (data: IpcChannels[T]['response']) => void ) => { const subscription = (_: Electron.IpcRendererEvent, data: IpcChannels[T]['response']) => { callback(data); }; ipcRenderer.on(channel, subscription); return () => ipcRenderer.removeListener(channel, subscription); }, };
contextBridge.exposeInMainWorld('electron', electronAPI);
Pattern 3: Type-Safe IPC Channels
Define all channels with request/response types:
// shared/types.ts export interface IpcChannels { 'app:get-version': { request: void; response: string; }; 'file:read': { request: { path: string }; response: { content: string } | { error: string }; }; 'file:write': { request: { path: string; content: string }; response: { success: boolean }; }; 'dialog:open-file': { request: { filters?: Electron.FileFilter[] }; response: string | null; }; 'store:get': { request: { key: string }; response: unknown; }; 'store:set': { request: { key: string; value: unknown }; response: void; }; }
// Extend Window interface for renderer declare global { interface Window { electron: typeof electronAPI; } }
Code Examples
Example 1: Main Process Setup
// main/index.ts import { app, BrowserWindow, ipcMain } from 'electron'; import path from 'path'; import { registerIpcHandlers } from './ipc/handlers';
let mainWindow: BrowserWindow | null = null;
async function createWindow() { mainWindow = new BrowserWindow({ width: 1200, height: 800, minWidth: 800, minHeight: 600, webPreferences: { preload: path.join(__dirname, '../preload/index.js'), contextIsolation: true, // Required for security nodeIntegration: false, // Required for security sandbox: true, // Extra security }, titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default', trafficLightPosition: { x: 15, y: 10 }, });
// Register IPC handlers registerIpcHandlers();
// Load the app if (process.env.NODE_ENV === 'development') { mainWindow.loadURL('http://localhost:5173'); mainWindow.webContents.openDevTools(); } else { mainWindow.loadFile(path.join(__dirname, '../renderer/index.html')); }
mainWindow.on('closed', () => { mainWindow = null; }); }
app.whenReady().then(createWindow);
app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } });
app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } });
Example 2: IPC Handlers
// main/ipc/handlers.ts import { ipcMain, dialog, app } from 'electron'; import fs from 'fs/promises'; import Store from 'electron-store';
const store = new Store();
export function registerIpcHandlers() { // Get app version ipcMain.handle('app:get-version', () => { return app.getVersion(); });
// File operations ipcMain.handle('file:read', async (_, { path }) => { try { const content = await fs.readFile(path, 'utf-8'); return { content }; } catch (error) { return { error: (error as Error).message }; } });
ipcMain.handle('file:write', async (_, { path, content }) => { try { await fs.writeFile(path, content, 'utf-8'); return { success: true }; } catch { return { success: false }; } });
// Native dialogs ipcMain.handle('dialog:open-file', async (_, { filters }) => { const result = await dialog.showOpenDialog({ properties: ['openFile'], filters: filters || [{ name: 'All Files', extensions: ['*'] }], }); return result.canceled ? null : result.filePaths[0]; });
// Persistent storage ipcMain.handle('store:get', (_, { key }) => { return store.get(key); });
ipcMain.handle('store:set', (_, { key, value }) => { store.set(key, value); }); }
Example 3: React Hook for IPC
// renderer/src/hooks/useIPC.ts import { useCallback, useEffect, useState } from 'react';
export function useIPC<T>( channel: string, initialValue: T ): [T, boolean, Error | null] { const [data, setData] = useState<T>(initialValue); const [loading, setLoading] = useState(true); const [error, setError] = useState<Error | null>(null);
useEffect(() => { let mounted = true;
window.electron
.invoke(channel, undefined)
.then((result) => {
if (mounted) {
setData(result as T);
setLoading(false);
}
})
.catch((err) => {
if (mounted) {
setError(err);
setLoading(false);
}
});
return () => {
mounted = false;
};
}, [channel]);
return [data, loading, error]; }
// Hook for IPC subscriptions export function useIPCListener<T>( channel: string, callback: (data: T) => void ) { useEffect(() => { const unsubscribe = window.electron.on(channel, callback); return unsubscribe; }, [channel, callback]); }
// Hook for IPC mutations export function useIPCMutation<TRequest, TResponse>(channel: string) { const [loading, setLoading] = useState(false); const [error, setError] = useState<Error | null>(null);
const mutate = useCallback( async (data: TRequest): Promise<TResponse | null> => { setLoading(true); setError(null); try { const result = await window.electron.invoke(channel, data); return result as TResponse; } catch (err) { setError(err as Error); return null; } finally { setLoading(false); } }, [channel] );
return { mutate, loading, error }; }
Example 4: Auto-Updater Setup
// main/services/updater.ts import { autoUpdater } from 'electron-updater'; import { BrowserWindow } from 'electron'; import log from 'electron-log';
export function setupAutoUpdater(mainWindow: BrowserWindow) { autoUpdater.logger = log; autoUpdater.autoDownload = false; autoUpdater.autoInstallOnAppQuit = true;
autoUpdater.on('checking-for-update', () => { mainWindow.webContents.send('updater:checking'); });
autoUpdater.on('update-available', (info) => { mainWindow.webContents.send('updater:available', info); });
autoUpdater.on('update-not-available', () => { mainWindow.webContents.send('updater:not-available'); });
autoUpdater.on('download-progress', (progress) => { mainWindow.webContents.send('updater:progress', progress); });
autoUpdater.on('update-downloaded', () => { mainWindow.webContents.send('updater:downloaded'); });
autoUpdater.on('error', (error) => { mainWindow.webContents.send('updater:error', error.message); });
// Check for updates on startup (with delay) setTimeout(() => { autoUpdater.checkForUpdates(); }, 5000); }
// IPC handlers for updater export function registerUpdaterHandlers() { ipcMain.handle('updater:check', () => autoUpdater.checkForUpdates()); ipcMain.handle('updater:download', () => autoUpdater.downloadUpdate()); ipcMain.handle('updater:install', () => autoUpdater.quitAndInstall()); }
Example 5: Native Menu Setup
// main/menu.ts import { Menu, shell, app, BrowserWindow } from 'electron';
export function createMenu(mainWindow: BrowserWindow) { const isMac = process.platform === 'darwin';
const template: Electron.MenuItemConstructorOptions[] = [ ...(isMac ? [{ label: app.name, submenu: [ { role: 'about' as const }, { type: 'separator' as const }, { role: 'services' as const }, { type: 'separator' as const }, { role: 'hide' as const }, { role: 'hideOthers' as const }, { role: 'unhide' as const }, { type: 'separator' as const }, { role: 'quit' as const }, ], }] : []), { label: 'File', submenu: [ { label: 'Open File', accelerator: 'CmdOrCtrl+O', click: () => mainWindow.webContents.send('menu:open-file'), }, { label: 'Save', accelerator: 'CmdOrCtrl+S', click: () => mainWindow.webContents.send('menu:save'), }, { type: 'separator' }, isMac ? { role: 'close' } : { role: 'quit' }, ], }, { label: 'Edit', submenu: [ { role: 'undo' }, { role: 'redo' }, { type: 'separator' }, { role: 'cut' }, { role: 'copy' }, { role: 'paste' }, { role: 'selectAll' }, ], }, { label: 'View', submenu: [ { role: 'reload' }, { role: 'forceReload' }, { role: 'toggleDevTools' }, { type: 'separator' }, { role: 'togglefullscreen' }, ], }, { label: 'Help', submenu: [ { label: 'Documentation', click: () => shell.openExternal('https://example.com/docs'), }, ], }, ];
Menu.setApplicationMenu(Menu.buildFromTemplate(template)); }
Anti-Patterns
Don't: Enable nodeIntegration
// ❌ DANGEROUS - Never do this const win = new BrowserWindow({ webPreferences: { nodeIntegration: true, // Security vulnerability! contextIsolation: false, // Security vulnerability! }, });
// ✅ Safe - Always use contextIsolation with preload const win = new BrowserWindow({ webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false, sandbox: true, }, });
Don't: Use Remote Module
// ❌ Bad - remote is deprecated and insecure const { BrowserWindow } = require('@electron/remote');
// ✅ Good - Use IPC for all main process access // In renderer: const result = await window.electron.invoke('dialog:open-file', {});
Don't: Expose Entire ipcRenderer
// ❌ Bad - exposes everything contextBridge.exposeInMainWorld('electron', { ipcRenderer: ipcRenderer, // Never expose the entire module! });
// ✅ Good - expose only specific, typed methods
contextBridge.exposeInMainWorld('electron', {
invoke: (channel: string, data: unknown) => {
const allowedChannels = ['app:get-version', 'file:read'];
if (allowedChannels.includes(channel)) {
return ipcRenderer.invoke(channel, data);
}
throw new Error(Channel ${channel} not allowed);
},
});
Quick Reference
Task Pattern
Create project npm create electron-vite@latest
Main process file access Use Node.js fs module in main
Renderer file access IPC through preload
Persistent storage electron-store in main process
Auto-updates electron-updater
Native notifications new Notification() in main
System tray Tray class in main
Keyboard shortcuts globalShortcut.register()
Deep linking app.setAsDefaultProtocolClient()
Code signing electron-builder config
Resources
-
Electron Documentation
-
Electron Forge
-
electron-vite
-
electron-builder
-
Electron Security Checklist