@egoist/tipc
Typed IPC communication for Electron Apps.
Install
Calling main from renderer
Create a TIPC router:
// main/tipc.ts import { tipc } from "@egoist/tipc/main" const t = tipc.create() export const router = { sum: t.procedure .input<{ a: number; b: number }>() .action(async ({ input }) => { return input.a + input.b }), } export type Router = typeof router
In Electron main process:
// main/index.ts import { registerIpcMain } from "@egoist/tipc/main" import { router } from "./tipc" registerIpcMain(router)
This will register all the TIPC router methods as IPC handlers using Electron's ipcMain.handle.
In Electron renderer, create a TIPC client:
// renderer/client.ts import { createClient } from "@egoist/tipc/renderer" import { Router } from "../main/tipc" export const client = createClient<Router>({ // pass ipcRenderer.invoke function to the client // you can expose it from preload.js in BrowserWindow ipcInvoke: window.ipcRenderer.invoke, })
Now you can call the TIPC methods in renderer process directly:
client.sum({ a: 1, b: 2 }).then(console.log) // 3
With React Query
Replace the renderer/client.ts with the following code:
//renderer/client.ts import { createClient } from "@egoist/tipc/react-query" import { Router } from "../main/tipc" export const client = createClient<Router>({ ipcInvoke: window.ipcRenderer.invoke, })
Now you can use React Query methods:
const sumMutation = client.sum.useMutation() sumMutation.mutate({ a: 1, b: 2 })
It's up to you to whether use the TIPC method as a query or mutation, you can use call useQuery:
const sumQuery = client.sum.useQuery({ a: 1, b: 2 }) sumQuery.data // 3 or undefined sumQuery.isLoading // boolean
Access sender in TIPC methods
export const router = { hello: t.procedure.action(async ({ context }) => { // sender is a WebContents instance that is calling this method context.sender.send("some-event") return `Hello, ${input.name}` }), }
Calling renderer from main
Define event handlers type for the renderer:
// main/renderer-handlers.ts export type RendererHandlers = { helloFromMain: (message: string) => void }
Send events from main to renderer:
// main/index.ts import { getRendererHandlers } from "@egoist/tipc/main" import { RendererHandlers } from "./renderer-handlers" const window = new BrowserWindow({}) const handlers = getRendererHandlers<RendererHandlers>(window.webContents) handlers.helloFromMain.send("Hello from main!")
But you also need to listen to events in renderer:
// renderer/tipc.ts import { createEventHandlers } from "@egoist/tipc/renderer" import { RendererHandlers } from "../main/renderer-handlers" export const handlers = createEventHandlers<RendererHandlers>({ // when using electron's ipcRenderer directly on: (channel, callback) => { window.ipcRenderer.on(channel, callback) return () => { window.ipcRenderer.off(channel, callback) } }, // otherwise if using @electron-toolkit/preload or electron-vite // which expose a custom `on` method that does the above for you // on: window.electron.ipcRenderer.on, send: window.ipcRenderer.send, })
Let's say you're using React, you can now listen to events in your component:
//renderer/app.tsx import { handlers } from "./tipc" useEffect(() => { const unlisten = handlers.helloFromMain.listen((message) => { console.log(message) }) return unlisten }, [])
Get response from renderer
The .send method only send a message to renderer, if you want to get a response from renderer, you can use .invoke method:
// main/index.ts const handlers = getRendererHandlers<RendererHandlers>(window.webContents) handlers.calculateInRenderer.invoke(1, 2).then(console.log)
// renderer/app.tsx useEffect(() => { const unlisten = handlers.calculateInRenderer.handle((left, right) => { return left + right }) return unlisten }, [])
License
MIT.