Local data and native APIs
Export with a native dialog
Use Electron’s save dialog in the main process and write a user-selected JSON export file.
Export is our first native feature. The user clicks a button and the operating system’s own save dialog appears.
The renderer asks. The main process does everything else: it opens the dialog, reads the path the user picked, and writes the file. The renderer never learns anything about the filesystem.
Add this handler to src/index.js:
const { dialog } = require('electron')
ipcMain.handle('notes:export', async event => {
assertTrustedSender(event.senderFrame)
const notes = await readNotes(notesPath(), validateNotes)
const result = await dialog.showSaveDialog(mainWindow, {
defaultPath: 'desktop-notes.json',
filters: [{ name: 'JSON', extensions: ['json'] }]
})
if (result.canceled || !result.filePath) return { canceled: true }
await writeNotes(result.filePath, notes)
return { canceled: false }
})
dialog.showSaveDialog() returns a promise. It resolves with an object that has canceled and, when the user confirmed, filePath.
Passing mainWindow as the first argument makes the dialog modal: it attaches to the notes window and blocks it until the user answers. Without it the dialog floats free, which looks wrong on macOS.
defaultPath suggests a filename. filters limits the dialog to .json files.
Why the renderer can’t pick the path
Be careful here. It would be easy to let the renderer send a path and have the main process write there. Don’t. That turns a narrow “export my notes” feature into “write any file anywhere on disk”. If an attacker ever gets a script into the renderer, that’s exactly the primitive they want.
The user chooses the path through a system dialog the renderer cannot control. That’s the point.
Export what’s saved, not what’s on screen
Notice that the handler reads the notes from disk and validates them, instead of accepting a notes array from the renderer. The export reflects what’s durable. An unsaved edit, or a value that failed validation, doesn’t end up in the file.
Wire the button
Add an Export button to index.html and call the bridge from renderer.js:
const exportButton = document.querySelector('#export')
exportButton.addEventListener('click', async () => {
const result = await window.notesAPI.exportNotes()
status.textContent = result.canceled ? 'Export canceled' : 'Notes exported'
})
Cancel is a normal outcome, not an error. Test all four paths: cancel, confirming the overwrite of an existing file, an unwritable destination, and a successful export. Then open the resulting JSON in another program to confirm the contents.
One thing to remember for sensitive notes: export creates a second copy the app no longer manages. Say so in the interface, and never upload it anywhere silently.
Lesson completed