Typed IPC communication for Electron Apps.
npm i @egoist/tipc
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
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
sender
in TIPC methodsexport 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}`
}),
}
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
}, [])
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
}, [])
MIT.