Local data and native APIs

Add an application menu

Create a cross-platform menu with native roles and send a narrow New Note event to the renderer.

Every desktop app has a menu bar. Electron builds it from a template, a plain array of objects that describes menus and items.

The nice part is roles. A role is a built-in menu item Electron knows how to render on each platform. { role: 'quit' } becomes “Quit Desktop Notes” with ⌘Q on macOS and “Exit” on Windows, with the right shortcut and behavior. You write none of that.

Add this function to src/index.js:

const { Menu } = require('electron')

function installMenu(win) {
  const template = [
    ...(process.platform === 'darwin' ? [{ role: 'appMenu' }] : []),
    {
      label: 'File',
      submenu: [
        {
          label: 'New Note',
          accelerator: 'CmdOrCtrl+N',
          click: () => {
            if (!win.isDestroyed()) {
              win.webContents.send('notes:new')
            }
          }
        },
        { role: 'quit' }
      ]
    },
    { role: 'editMenu' },
    { role: 'viewMenu' },
    { role: 'windowMenu' }
  ]

  Menu.setApplicationMenu(Menu.buildFromTemplate(template))
}

Call installMenu(win) right after creating the window in createWindow().

Let’s look at the parts.

The spread on the first line adds { role: 'appMenu' } only on macOS. That’s the menu named after the app, with About, Hide, and Quit. Windows and Linux don’t have it, so we skip it there.

The File menu has our one custom item. CmdOrCtrl+N means ⌘N on macOS and Ctrl+N everywhere else. Its click handler sends one fixed event, notes:new, to the window’s web contents.

editMenu, viewMenu, and windowMenu give you Undo, Copy, Paste, Reload, Zoom, Minimize, and friends. Handwritten copies of these are always worse. Use the roles.

The path of a menu click

The menu lives in the main process. When you press ⌘N, the main process calls win.webContents.send('notes:new'). The preload script we wrote earlier receives it, drops Electron’s event object, and calls the renderer’s createNote(). The renderer gets a plain signal and nothing more.

Notice the isDestroyed() guard. On macOS the menu stays alive after the user closes the window. Sending to destroyed web contents throws, so we check first.

One implementation, not two

If a menu item does privileged work, like Save or Export, don’t write a second version of that logic in the click handler. Call the same validated function the IPC handler calls. Two implementations drift apart, and the menu one is the one nobody tests.

Test the item with the mouse and with the shortcut. Then close the window on macOS and press ⌘N again: nothing should crash, because the guard stops the send.

Lesson completed