Electron + React Best Practices
Guide AI agents in building secure, production-ready Electron applications with React. This skill provides security patterns, type-safe IPC communication, project setup guidance, packaging and code signing workflows, and tools for analysis, scaffolding, and type generation.
When to Use This Skill
Use this skill when:
- Generating Electron main, preload, or renderer process code
- Configuring electron-vite or Electron Forge
- Setting up IPC communication between processes
- Implementing security patterns (contextBridge, sandbox, CSP)
- Packaging, signing, and notarizing desktop applications
- Testing Electron apps with Playwright
- Designing multi-window architectures
Do NOT use this skill when:
- Building Tauri apps (different paradigm, use Tauri-specific guidance)
- Building pure web apps with no desktop requirements
- Targeting Electron versions below 20 (security defaults differ)
- Using non-React renderer frameworks (use framework-specific skills)
Core Principles
1. Security First Architecture
Modern Electron security relies on three defaults that became standard in Electron 20+: context isolation, sandbox mode, and nodeIntegration disabled. Disabling any of them allows XSS attacks to escalate to full remote code execution. All main-renderer communication must flow through contextBridge:
contextBridge.exposeInMainWorld('electronAPI', {
loadPreferences: () => ipcRenderer.invoke('load-prefs'),
saveFile: (content: string) => ipcRenderer.invoke('save-file', content),
onUpdateCounter: (callback: (value: number) => void) => {
const handler = (_event: IpcRendererEvent, value: number) => callback(value);
ipcRenderer.on('update-counter', handler);
return () => ipcRenderer.removeListener('update-counter', handler);
}
});
Set Content Security Policy via HTTP headers for apps loading local files, restricting script sources to 'self'.
2. Type-Safe IPC Communication
The invoke/handle pattern is preferred over send/on for request-response communication, providing proper async/await semantics and error propagation. For typed channels, use a mapped type pattern:
type IpcChannelMap = {
'load-prefs': { args: []; return: UserPreferences };
'save-file': { args: [content: string]; return: { success: boolean } };
};
For complex applications, electron-trpc provides full type safety using tRPC's router pattern with Zod validation:
export const appRouter = t.router({
greeting: t.procedure
.input(z.object({ name: z.string() }))
.query(({ input }) => `Hello, ${input.name}!`),
});
Error handling across the IPC boundary requires attention because Electron only serializes the message property of Error objects. Wrap responses in a { success, data, error } result type to preserve full error context.
3. Modern Project Setup
The recommended stack uses electron-vite for development and Electron Forge for packaging. electron-vite provides a unified configuration managing main, preload, and renderer processes with sub-second dev server startup and instant HMR. Electron Forge uses first-party Electron packages for signing and notarization.
src/
βββ main/ # Main process (Node.js environment)
β βββ index.ts
β βββ ipc/ # IPC handlers
βββ preload/ # Secure bridge via contextBridge
β βββ index.ts
β βββ index.d.ts # TypeScript declarations for exposed APIs
βββ renderer/ # React application (pure web, no Node access)
βββ src/
βββ index.html
4. React Integration Patterns
React 18's concurrent features work normally in Electron's Chromium-based renderer. Strict Mode's double-invocation of effects catches IPC listener leaks that would otherwise cause memory issues. Always return cleanup functions from effects that register IPC listeners:
useEffect(() => {
const cleanup = window.electronAPI.onUpdateCounter((value) => {
setCount(value);
});
return cleanup;
}, []);
For multi-window applications, the main process should serve as the single source of truth for shared state. Use electron-store for persistence combined with IPC broadcasting so any window's mutation updates all others.
Quick Reference
| Category |
Prefer |
Avoid |
| Security |
contextBridge.exposeInMainWorld() |
nodeIntegration: true |
| IPC |
invoke/handle pattern |
send/on for request-response |
| Preload |
Typed function wrappers |
Exposing raw ipcRenderer |
| Build tool |
electron-vite |
webpack-based toolchains |
| Packaging |
Electron Forge |
Manual packaging |
| State |
Zustand + electron-store |
Redux for simple apps |
| Testing |
Playwright E2E |
Spectron (deprecated) |
| Updates |
electron-updater |
Manual update checks |
| Signing |
CI-integrated code signing |
Unsigned releases |
| CSP |
HTTP headers, 'self' only |
No CSP |
| Error handling |
Result type {success, data, error} |
Raw Error across IPC |
| Multi-window |
Main process as state hub |
Direct window-to-window |
Code Generation Guidelines
When generating Electron code, follow these patterns:
BrowserWindow Creation
const win = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
contextIsolation: true,
sandbox: true,
nodeIntegration: false,
},
});
Always enable contextIsolation and sandbox. Never enable nodeIntegration. The preload path must resolve to the built output location.
IPC Handler Module
export function registerFileHandlers(): void {
ipcMain.handle('save-file', async (_event, content: string) => {
try {
await fs.writeFile(filePath, content);
return { success: true, data: filePath };
} catch (err) {
return { success: false, error: (err as Error).message };
}
});
}
Group related handlers into modules. Use the result type pattern for all return values. Validate all arguments received from the renderer process.
Common Anti-Patterns
Avoid these patterns when generating Electron code:
| Anti-Pattern |
Problem |
Solution |
nodeIntegration: true |
XSS escalates to full RCE |
Keep disabled (default) |
Exposing ipcRenderer directly |
Full IPC access from renderer |
Wrap in contextBridge functions |
Missing contextIsolation |
Renderer accesses preload scope |
Keep enabled (default since Electron 12) |
| No code signing |
OS security warnings, Gatekeeper blocks |
Sign and notarize for all platforms |
BrowserWindow without sandbox |
Preload has full Node.js access |
Enable sandbox (default since Electron 20) |
| Unvalidated IPC arguments |
Injection attacks from renderer |
Validate with Zod or manual checks |
0.0.0.0 server binding |
Network-exposed local server |
Always bind to 127.0.0.1 |
| Missing CSP headers |
Script injection vectors |
Set strict CSP via HTTP headers |
| No IPC error serialization |
Lost error context across boundary |
Use Result type pattern |
| Spectron for testing |
Deprecated, Electron 13 max |
Use Playwright |
See references/security/security-checklist.md for the full security audit checklist.
Scripts Reference
analyze-security.ts
Analyze Electron projects for security misconfigurations:
deno run --allow-read scripts/analyze-security.ts <path> [options]
Options:
--strict Enable all checks
--json Output JSON for CI
-h, --help Show help
Examples:
deno run --allow-read scripts/analyze-security.ts ./src
deno run --allow-read scripts/analyze-security.ts ./src --strict --json
scaffold-electron-app.ts
Scaffold a new Electron + React project with secure defaults:
deno run --allow-read --allow-write scripts/scaffold-electron-app.ts [options]
Options:
--name <name> App name (required)
--path <path> Target directory (default: ./)
<