name: electron-react description: 'Build Electron desktop apps with React, Vite, and TypeScript. Use when creating main/renderer processes, setting up IPC communication, configuring electron-vite, implementing preload scripts, or building desktop UI with React. Triggers on Electron app, desktop app, IPC, preload, context bridge, main process, renderer process.'
Electron + React Development
Build modern desktop applications with Electron, React, and TypeScript using electron-vite for optimal developer experience.
When to Use This Skill
- Creating new Electron applications with React
- Setting up IPC (Inter-Process Communication) between main and renderer
- Implementing preload scripts with context bridge
- Configuring electron-vite for development and production builds
- Building type-safe communication layers
Project Structure
apps/electron/
├── src/
│ ├── main/ # Electron main process
│ │ ├── index.ts # Entry point, window creation
│ │ └── ipc/ # IPC handlers
│ ├── preload/ # Context bridge scripts
│ │ ├── index.ts # Exposed APIs
│ │ └── types.d.ts # TypeScript declarations
│ └── renderer/ # React application
│ ├── index.html
│ ├── main.tsx
│ └── components/
├── electron.vite.config.ts
└── package.json
Core Patterns
Main Process Entry Point
// src/main/index.ts
import { app, BrowserWindow, ipcMain } from 'electron';
import path from 'path';
let mainWindow: BrowserWindow | null = null;
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
contextIsolation: true, // Required for security
nodeIntegration: false, // Keep disabled
sandbox: true,
},
});
// Load the renderer
if (process.env.NODE_ENV === 'development') {
mainWindow.loadURL('http://localhost:5173');
mainWindow.webContents.openDevTools();
} else {
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'));
}
}
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
Type-Safe IPC Layer
// packages/core/src/ipc-types.ts
export interface IPCChannels {
// Request-Response patterns
'db:tasks:list': { request: void; response: Task[] };
'db:tasks:create': { request: CreateTaskInput; response: Task };
'db:tasks:update': { request: UpdateTaskInput; response: Task };
'db:tasks:delete': { request: string; response: void };
// Streaming patterns (main -> renderer)
'agent:session:stream': { request: string; response: AgentStep };
}
export type IPCChannel = keyof IPCChannels;
Preload Script with Context Bridge
// src/preload/index.ts
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron';
import type { IPCChannels, IPCChannel } from '@agentop/core';
type RequestType<K extends IPCChannel> = IPCChannels[K]['request'];
type ResponseType<K extends IPCChannel> = IPCChannels[K]['response'];
const api = {
// Type-safe invoke for request-response
invoke: <K extends IPCChannel>(
channel: K,
...args: RequestType<K> extends void ? [] : [RequestType<K>]
): Promise<ResponseType<K>> => {
return ipcRenderer.invoke(channel, ...args);
},
// Type-safe streaming subscription
on: <K extends IPCChannel>(
channel: K,
callback: (data: ResponseType<K>) => void
): (() => void) => {
const handler = (_event: IpcRendererEvent, data: ResponseType<K>) => {
callback(data);
};
ipcRenderer.on(channel, handler);
return () => ipcRenderer.removeListener(channel, handler);
},
// One-time listener
once: <K extends IPCChannel>(
channel: K,
callback: (data: ResponseType<K>) => void
): void => {
ipcRenderer.once(channel, (_event, data) => callback(data));
},
};
contextBridge.exposeInMainWorld('api', api);
// Type declarations for renderer
declare global {
interface Window {
api: typeof api;
}
}
IPC Handler Registration (Main Process)
// src/main/ipc/handler.ts
import { ipcMain, BrowserWindow } from 'electron';
import type { IPCChannels, IPCChannel } from '@agentop/core';
type RequestType<K extends IPCChannel> = IPCChannels[K]['request'];
type ResponseType<K extends IPCChannel> = IPCChannels[K]['response'];
export function registerHandler<K extends IPCChannel>(
channel: K,
handler: (request: RequestType<K>) => Promise<ResponseType<K>> | ResponseType<K>
) {
ipcMain.handle(channel, async (_event, request) => {
return handler(request);
});
}
// Streaming to renderer
export function sendToRenderer<K extends IPCChannel>(
window: BrowserWindow,
channel: K,
data: ResponseType<K>
) {
window.webContents.send(channel, data);
}
React Hooks for IPC
// src/renderer/hooks/useIPC.ts
import { useState, useCallback } from 'react';
import type { IPCChannels, IPCChannel } from '@agentop/core';
type RequestType<K extends IPCChannel> = IPCChannels[K]['request'];
type ResponseType<K extends IPCChannel> = IPCChannels[K]['response'];
export function useIPC<K extends IPCChannel>(channel: K) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const invoke = useCallback(
async (...args: RequestType<K> extends void ? [] : [RequestType<K>]) => {
setLoading(true);
setError(null);
try {
const result = await window.api.invoke(channel, ...args);
return result;
} catch (err) {
setError(err instanceof Error ? err : new Error(String(err)));
throw err;
} finally {
setLoading(false);
}
},
[channel]
);
return { invoke, loading, error };
}
// Streaming hook
export function useIPCStream<K extends IPCChannel>(
channel: K,
onData: (data: ResponseType<K>) => void
) {
useEffect(() => {
return window.api.on(channel, onData);
}, [channel, onData]);
}
electron-vite Configuration
// electron.vite.config.ts
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()],
build: {
rollupOptions: {
input: {
index: path.resolve(__dirname, 'src/main/index.ts'),
},
},
},
},
preload: {
plugins: [externalizeDepsPlugin()],
build: {
rollupOptions: {
input: {
index: path.resolve(__dirname, 'src/preload/index.ts'),
},
},
},
},
renderer: {
plugins: [react()],
root: path.resolve(__dirname, 'src/renderer'),
build: {
rollupOptions: {
input: {
index: path.resolve(__dirname, 'src/renderer/index.html'),
},
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src/renderer'),
'@agentop/core': path.resolve(__dirname, '../../packages/core/src'),
},
},
},
});
Security Best Practices
- Always enable contextIsolation - Prevents renderer from accessing Node.js
- Keep nodeIntegration disabled - Use preload scripts instead
- Use sandbox mode - Additional security layer
- Validate IPC inputs - Never trust renderer data
- Minimize exposed APIs - Only expose what's necessary
Development Commands
# Development with hot reload
make dev
# Build for production
make build
# Package for distribution
make package
Troubleshooting
| Issue | Solution |
|---|---|
| IPC not working | Ensure preload script is loaded, check contextIsolation |
| TypeScript errors in preload | Add preload types to tsconfig |
| Hot reload not working | Check vite dev server URL in main process |
| Build fails | Verify externalizeDepsPlugin for native modules |