Electron

【Electron】メインとレンダラー間通信で値を返す方法

更新日:2026/05/08

Electronでメインとレンダラー間で通信を行い、結果を受け取りたいケースがあります。
今回は、レンダラーからメインへの通信と、メインからレンダラーへの通信の二つにわけて、その方法について紹介します。

 

レンダラーからメインへの通信で結果を受け取る方法

レンダラーからメインへ通信を行い、メインからの結果をレンダラーで受けとる場合は、レンダラー側でipcRenderer.invoke()を使用します。

実装例

まずは、ipcRenderer.invoke()の受け取り側となるipcMain.handle()を、メイン側で呼び出します。
ipcMain.handle()の戻り値が、レンダラー側に渡されます。

メイン

ipcMain.handle('ping', () => '生きてるよ!')

次にレンダラーからメインに通信を送るのですが、レンダラー生成時のオプション指定によってはElectronのAPI呼び出しが制限されます。
そこで、プリロードでipcRenderer.invoke()を呼び出す関数を定義して公開します。

プリロード

import { contextBridge, ipcRenderer } from 'electron'

const api = {
  ping: async (): Promise<string> => await ipcRenderer.invoke('ping')
}

contextBridge.exposeInMainWorld('api', api)

最後に、レンダラー側でプリロードが公開した関数を呼び出します。

レンダラー

const result = await window.api.ping()
console.log(result)

これでOK

タイムアウト処理

ipcRenderer.invoke()は、メインからの応答があるまで待ち続けます。
そのため、場合によってはタイムアウト処理を実装する必要がありますね。

ipcRenderer.invoke()の戻り値はPromiseなので、setTimeoutを組み込んだPromiseを用意して、Promise.race()で二つのPromiseのどちらかの結果が出るのを待つ方法でタイムアウトを実現できます。

interface InvokeWithTimeoutResult {
  error: null | string
  result: any
}
const invokeWithTimeout = async (
  channel: string,
  timeout: number,
  ...arg: any[]
): Promise<InvokeWithTimeoutResult> => {
  try {
    const invokePromise = ipcRenderer.invoke(channel, ...arg)

    let timeoutId: null | NodeJS.Timeout = null

    const timeoutPromise = new Promise(
      (_, reject) => (timeoutId = setTimeout(() => reject(new Error('Timeout')), timeout))
    )

    const result = await Promise.race([invokePromise, timeoutPromise])

    if (timeoutId) clearTimeout(timeoutId)

    return { error: null, result }

  } catch (error: unknown) {
    const message = error instanceof Error ? error.message : 'unknown error'
    return { error: message, result: null }
  }
}

プリロードのコードを上記関数に置き換えます。

const api = {
  ping: async (): Promise<InvokeWithTimeoutResult> => invokeWithTimeout('ping', 3000)
}

ipcRenderer.invoke()はキャンセルできないので、タイムアウト後も応答を待ち続けます。
レンダラー側はメインからの応答を無視する形になります。

メイン側の処理を止めたいときは、

1. setTimeout()のコールバック内でipcRenderer.send()等でメインに停止要求を通知
2. メイン側は停止要求を受け取ったら処理を中断して何らかの値を返す

といった処理を行うとよいですね。

 

メインからレンダラーへの通信で結果を受け取る方法

メインからレンダラーへ通信を行い、レンダラーからの結果をメインで受けとる場合は、少しめんどうです。
レンダラー側のような、送信から結果受け取りまでをサポートした関数が用意されていないからです。

そこでメインからレンダラーへ一方通行でメッセージを送ります。
次にレンダラーからの通信を待ち受けるリスナーを登録して、レンダラーからの通信を待ちます。

実装例

まずはメイン側。
レンダラー側のビューにsend()でメッセージを送ります。
そして、ipcMain.once()で一回だけレンダラーからの通信を待ちます。

ただし同じchannelでの応答待ちが並列する可能性があります。
この場合、並列側の応答を受け取ってしまう危険性があります。

そこでメイン側からレンダラー側に応答channelを送信して、channelが重複しないようにします。

メイン

const invokeRenderer = (
  webContents: Electron.WebContents,
  channel: string,
  ...arg: any[]
): Promise<any> => {
  return new Promise((resolve) => {
    const id = randomUUID()
    const channelId = `${channel}:${id}` // レンダラー側の応答channel

    webContents.send(channel, channelId, ...arg)

    ipcMain.once(channelId, (_: Electron.IpcMainEvent, ...args: any[]): void => {
      const result = args.length === 0 ? null : args.length === 1 ? args[0] : args
      resolve(result)
    })

  })
}

作成した関数をメイン側で呼び出します。

メイン

  const mainWindow = new BaseWindow({ /* 設定省略 */ })
  const webView = new WebContentsView({/* 設定省略 */ })
  mainWindow.contentView.addChildView(webView)

.... // その他の処理

  const result = await invokeRenderer(webView.webContents, 'Are you ready?')
  console.log(result)

プリロードはipcRenderer.on()でメインからの通信を待ち受け、ipcRenderer.send()でメインに送信する関数を定義して公開します。

プリロード

import { contextBridge, ipcRenderer } from 'electron'

// Custom APIs for renderer
const api = {
  onMain: (callback) =>
    ipcRenderer.on('Are you ready?', async (_, channelId) => {
      const result = await callback()
      ipcRenderer.send(channelId, result)
    }),
}

contextBridge.exposeInMainWorld('api', api)

最後に、レンダラー側でプリロードが公開した関数を呼び出します。

レンダラー

window.api.onMain(() => {
  return 'OK!!'
})

レンダラー側のwindow.api.onMain()呼び出しは、一か所のみにする必要があります。
複数あるとメインへの応答が複数になるので、メイン側で受けることができない応答が存在することになるからです。

他にも方法がありそうですね。

例えば毎回ipcMain.once()で待つのが非効率と感じるかもしれません。
その場合は、channelIdをキー、resolveを値としてMapに保存。
あらかじめ呼び出しておいたipcMain.on()でレンダー側の応答を受け取り、Mapに保存したresolveを呼び出すという流れにすると良いかもしれません。

タイムアウト処理

ipcMain.once()もipcRenderer.invoke()と同様に応答があるまで待ち続けるので、場合によってはタイムアウト処理が必要ですね。
こちらもsetTimeout()でタイムアウトを実装できます。

interface InvokeRendererWithTimeoutResult {
  error: null | string
  result: any
}

const invokeRenderer = (
  webContents: Electron.WebContents,
  channel: string,
  timeout: number,
  ...arg: any[]
): Promise<InvokeRendererWithTimeoutResult> => {
  return new Promise((resolve) => {
    let timeoutId: null | NodeJS.Timeout = null

    const listener = (_: Electron.IpcMainEvent, ...args: any[]): void => {
      if (timeoutId) clearTimeout(timeoutId)
      const result = args.length === 0 ? null : args.length === 1 ? args[0] : args
      resolve({ error: null, result })
    }

    const id = randomUUID()
    const channelId = `${channel}:${id}` // レンダラー側の応答channel

    webContents.send(channel, channelId, ...arg)

    ipcMain.once(channelId, listener)

    timeoutId = setTimeout(() => {
      ipcMain.off(channelId, listener)
      resolve({ error: 'Timeout', result: null })
    }, timeout)
  })
}

ipcMain.once()はipcMain.off()で解除できます。

 

双方向通信まとめ

■レンダラーからメインへの通信の流れ

メイン方向レンダラー
ipcMain.handle()ipcRenderer.invoke()

■メインからレンダラーへの通信の流れ

メイン方向レンダラー
ビュー.webContents.send()ipcRenderer.on()
ipcMain.once()ipcRenderer.send()

更新日:2026/05/08

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。