Electron上のReactからFile Dialogを出す

1 min read読了の目安(約1600字

使うAPIはとても簡単なのですが、核心を突く記事が見つからない気がするので…

TL; DR

ipcMainipcRenderer を使ってメインプロセスに showOpenDialog させる

古い記事に当たると、レンダラからメインを直接参照するっぽい remote というものを使っていたりするのですが、これは deprecated なので使わないようにしましょう

使い方は Socket.io を使ったことのある方には直感的だと思います

main.ts
let mainWindow

const addIpcListener = () => {
  ipcMain.on('open-file-dialog', async (event, payload?: OpenDialogOptions) => {
    if (!mainWindow) {
      event.reply('open-file-dialog')
      return
    }

    const value = await dialog.showOpenDialog(mainWindow, {
      ...payload
    })
    event.reply('open-file-dialog', value)
  })
}

const createWindow = async () => {
  mainWindow = new BrowserWindow({ ... })
  addIpcListener()
}
renderer.ts
ipcRenderer.on('open-file-dialog', (_event, result) => {
  if (result.canceled) {
    return
  }
  
  const filePaths = result.filePaths
  // 処理
})

ipcRenderer.send('open-file-dialog', {
  title: 'Select a file',
  filters: [{
    name: 'CSV file',
    extensions: ['csv']
  }]
})

Channelと呼ばれる文字列 (ここでは 'open-file-dialog') の一致したリスナが呼ばれます
レンダラから send するとメインの on に渡したコールバックが呼ばれて、その中で reply するとさらにメインからレンダラにオブジェクトを返すことができます

通信内容はたぶんJSONかなにかなので、シリアライズ不可能なオブジェクト (循環参照を持つものなど) は怒られます

Tips

レンダラ側ではファイルが返ってくるまで待ちたかったりするので、こういう関数を書くと捗ると思います

ipc.ts
const ipc = <T, U>(channel: Channel, payload?: T): Promise<U> => new Promise((resolve) => {
  ipcRenderer.once(channel, (_event, args: U) => {
    resolve(args)
  })

  ipcRenderer.send(channel, payload)
})

once は1度受け取るとremoveされるリスナ (参考: ipcRenderer)