Skip to content

Comparison with ipcMain/ipcRenderer

This page compares the traditional ipcMain/ipcRenderer approach with electron-messageport-trpc.

// main process
ipcMain.handle('greet', async (_event, name: string) => {
return `Hello, ${name}!`;
});
// renderer process (via preload)
const result = await ipcRenderer.invoke('greet', 'World');
// result is string, but TypeScript doesn't know that
  1. No type safety — channel names are strings; input and output types are unknown
  2. Star topology — all messages route through the main process
  3. JSON serializationipcRenderer.invoke uses JSON internally, losing Date, Map, Set, ArrayBuffer types
  4. Manual subscription management — you have to wire up ipcMain.on / webContents.send yourself
  5. No schema validation — invalid inputs are only caught at runtime (if at all)
// main process
const t = initTRPC.create();
const appRouter = t.router({
greet: t.procedure
.input(z.object({ name: z.string() }))
.query(({ input }) => `Hello, ${input.name}!`),
});
// renderer process
const trpc = createTRPCClient<AppRouter>({
links: [portLink({ port: getPort() })],
});
const result = await trpc.greet.query({ name: 'World' });
// result is fully typed as string
  1. End-to-end type safety — TypeScript validates inputs and outputs at compile time
  2. Flexible topology — MessagePort enables renderer-to-main and utility-process communication patterns
  3. Structured Clone — native transfer of Date, Map, Set, ArrayBuffer, Error, and more
  4. Built-in subscriptions — tRPC v11 async iterables work natively
  5. Schema validation — use Zod (or any validator) for runtime input validation
FeatureipcMain/ipcRendererelectron-messageport-trpc
Type safetyNoneFull end-to-end
Input validationManualZod / any tRPC validator
SerializationJSON.stringifyStructured Clone
TopologyStar (main only)Flexible (MessagePort)
SubscriptionsManual event wiringtRPC subscriptions
Error handlingManualtRPC error shapes
MiddlewareNonetRPC middleware chain
BatchingManualPossible via tRPC links
Context / AuthManualcreateContext option
// With ipcMain (JSON serialization)
ipcMain.handle('getData', () => {
return {
date: new Date(), // Becomes string "2024-01-01T00:00:00.000Z"
map: new Map([['a', 1]]), // Becomes {}
set: new Set([1, 2, 3]), // Becomes {}
buffer: new ArrayBuffer(8), // Becomes {}
};
});
// With electron-messageport-trpc (Structured Clone)
const router = t.router({
getData: t.procedure.query(() => {
return {
date: new Date(), // Stays a Date object
map: new Map([['a', 1]]), // Stays a Map
set: new Set([1, 2, 3]), // Stays a Set
buffer: new ArrayBuffer(8), // Stays an ArrayBuffer
};
}),
});

The traditional approach may still be appropriate when:

  • You have a very simple app with only a few IPC calls
  • You don’t need TypeScript type safety
  • You need to use Electron APIs that don’t support MessagePort (rare)
  • You’re maintaining legacy code and the migration cost is too high

For all other cases, electron-messageport-trpc provides a better developer experience and more robust communication.