electron-react

star 8

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.

macromania By macromania schedule Updated 1/29/2026

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

  1. Always enable contextIsolation - Prevents renderer from accessing Node.js
  2. Keep nodeIntegration disabled - Use preload scripts instead
  3. Use sandbox mode - Additional security layer
  4. Validate IPC inputs - Never trust renderer data
  5. 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
Install via CLI
npx skills add https://github.com/macromania/agentop --skill electron-react
Repository Details
star Stars 8
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator