Electron の main process と renderer process とのやりとりについてメモ

Inter-Process Communication
IPC (Inter-Process Communication) という言葉が出てくるが、これは main process と renderer process とのやりとりのこと

1. renderer → main
renderer から main の処理を呼び出すパターン

preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronApi', {
setTitle: (title) => ipcRenderer.send('set-title', title)
})
main.js
ipcMain.on('set-title', (event, title) => {
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
win.setTitle(title)
})
renderer (React)
const ChangeWindowTitle: React.FC = () => {
const [title, setTitle] = useState('');
return (
<div>
<input value={title} onChange={(e) => setTitle(e.target.value)} />
<button
onClick={() => {
window.electronApi.setTitle(title);
}}
>
Set title
</button>
</div>
);
};

何をやっているのか
preload.js
window に main.js 側の処理を呼び出すための api を生やしている。
ipcRenderer.send
で "main.js 側の event 処理を発火させている" イメージ。
ipcRenderer.send('set-title', title)
"set-title" という event を send している
main.js
renderer から呼び出したい処理を定義する。
ipcMain.on('set-title', (event, title) => {
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
win.setTitle(title)
})
'set-title' event が呼び出された時に、引数で渡された title の値に browser window の title を変更している。
renderer
あとは、renderer から window に生やした api を通じて main.js 側の処理を呼び出す。
<button
onClick={() => {
window.electronApi.setTitle(title);
}}
>
Set title
</button>


2. renderer → main → renderer
renderer から main の処理を呼び出して、その結果を main から renderer に返すパターン

preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronApi', {
openFile: () => ipcRenderer.invoke('dialog:openFile')
})
main.js
ipcMain.handle('dialog:openFile', async () => {
const { canceled, filePaths } = await dialog.showOpenDialog({});
if (!canceled) {
return filePaths;
}
});
renderer (React)
const SelectFile = () => {
const [filePath, setFilePath] = useState('');
return (
<div>
<div>Selected File: {filePath ?? 'None'}</div>
<button
onClick={async () => {
const filePath = await window.electronApi.openFile();
setFilePath(filePath);
}}
>
Open
</button>
</div>
);
};

何をやっているのか
preload.js
window に main.js 側の処理を呼び出すための api を生やしている。
ipcRenderer.invoke
はより直感的に "呼び出す" ための api 感がある。
ipcRenderer.invoke('dialog:openFile')
"dialog:openFile" という処理を invoke している。
main.js
renderer 側から invoke で呼び出せる処理を定義している。
ここで return した値を renderer 側へ受け渡すことができる。
ipcMain.handle('dialog:openFile', async () => {
const { canceled, filePaths } = await dialog.showOpenDialog({});
if (!canceled) {
return filePaths;
}
});
"dialog:openFile" では、file 選択 dialog を表示して、選択された file の path を返している。
renderer (React)
あとは、renderer 側から呼び出してあげればOK。
main.js 側から return された値を戻り値として取得できる。
<button
onClick={async () => {
const filePath = await window.electronApi.openFile();
setFilePath(filePath);
}}
>
Open
</button>


3. main → renderer
今度は、逆に main から renderer 側の処理を発火するパターン。

preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', callback)
})
main.js
const menu = Menu.buildFromTemplate([
{
label: app.name,
submenu: [
{
click: () => mainWindow.webContents.send('update-counter', 1),
label: 'Increment'
},
{
click: () => mainWindow.webContents.send('update-counter', -1),
label: 'Decrement'
}
]
}
])
Menu.setApplicationMenu(menu)
renderer (React)
const MenuCounter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
window.electronApi.onUpdateCounter((event, value) => {
setCount((v) => v + value);
});
}, []);
return (
<div>
<div>Count: {count}</div>
</div>
);
};

何をやっているのか
preload.js
ipcRenderer.on
で main 側から送信される event に callback を登録できる関数を生やす。
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', callback)
これで main 側から "update-counter" event が発火された場合、renderer 側の callback が呼び出される。
main.js
Desktop アプリの Menu を作成している。
Menu をクリックした際に、mainWindow.webContents.send('update-counter', 1)
が呼び出される。これで、renderer 側に "update-counter" event が送信される。
const menu = Menu.buildFromTemplate([
{
label: app.name,
submenu: [
{
click: () => mainWindow.webContents.send('update-counter', 1),
label: 'Increment',
},
{
click: () => mainWindow.webContents.send('update-counter', -1),
label: 'Decrement',
},
],
},
]);
renderer (React)
useEffect 内で "update-counter" event の際に発火される callback を登録している。
シンプルに、main 側から送信された値を元に counter の値を変化させている。
useEffect(() => {
window.electronApi.onUpdateCounter((event, value) => {
// increment の場合は value は 1
// decrement の場合は value は -1
setCount((v) => v + value);
});
}, []);



main → renderer → main
main から renderer 側の処理を発火したのち、renderer 側から main に結果を返したいケース。
やることとしては、1. renderer → main と同じで、
- renderer 側から main へ event を発火
-
ipcMain.on
で main 側で実行したい処理を実装
renderer
const counter = document.getElementById('counter')
window.electronAPI.onUpdateCounter((event, value) => {
const oldValue = Number(counter.innerText)
const newValue = oldValue + value
counter.innerText = newValue
event.sender.send('counter-value', newValue)
})
event.sender.send
で ``ipcRenderer.send` 同様に main 側へ event を送信できる。
main.js
main 側で renderer 側から発火したい処理を用意しておく。
// ...
ipcMain.on('counter-value', (_event, value) => {
console.log(value) // will print value to Node console
})
// ...