Desktop apps keep data on the user's machine: settings, recent files, and documents. This chapter covers where to store data, native APIs such as file dialogs and notifications, and how to split state between renderers and the main process.
App data paths
Each operating system keeps app data in a different place; app.getPath() returns the right directory. App-specific data belongs in userData.
const { app } = require('electron')
app.whenReady().then(() => {
console.log(app.getPath('userData'))
// Windows: C:\Users\<user>\AppData\Roaming\<app name>
// macOS: ~/Library/Application Support/<app name>
// Linux: ~/.config/<app name>
})
Never write user data next to the app itself (app.getAppPath() or the executable folder). After packaging, that location may be read-only or replaced on update.
Saving settings to a file
A single JSON file is enough for small settings. Do the file access in the main process and give renderers only read and write operations over IPC. Writing to a temporary file and renaming it keeps the file intact if the app quits mid-write.
// main.js
const { app, ipcMain, BrowserWindow } = require('electron')
const fs = require('node:fs/promises')
const path = require('node:path')
const settingsFile = () => path.join(app.getPath('userData'), 'settings.json')
const defaults = { theme: 'system', recentFiles: [] }
async function loadSettings() {
try {
return { ...defaults, ...JSON.parse(await fs.readFile(settingsFile(), 'utf8')) }
} catch {
return { ...defaults }
}
}
async function saveSettings(settings) {
const tmp = `${settingsFile()}.tmp`
await fs.writeFile(tmp, JSON.stringify(settings, null, 2))
await fs.rename(tmp, settingsFile())
}
ipcMain.handle('settings:get', () => loadSettings())
ipcMain.handle('settings:setTheme', async (_event, theme) => {
if (!['light', 'dark', 'system'].includes(theme)) throw new Error('invalid theme')
const settings = { ...(await loadSettings()), theme }
await saveSettings(settings)
// Tell every open window about the change
for (const win of BrowserWindow.getAllWindows()) win.webContents.send('settings:changed', settings)
return settings
})
For a key-value store, consider the community package electron-store; for larger data, an embedded database such as SQLite. Native modules like better-sqlite3 must be rebuilt against Electron, which Forge does for you (otherwise run npx @electron/rebuild).
File dialogs
The dialog module opens native dialogs: showOpenDialog, showSaveDialog, and showMessageBox for confirmations. Pass a window as the first argument to make the dialog modal to it.
const { dialog } = require('electron')
ipcMain.handle('file:save', async (event, content) => {
if (typeof content !== 'string') throw new Error('invalid content')
const win = BrowserWindow.fromWebContents(event.sender)
const { canceled, filePath } = await dialog.showSaveDialog(win, {
defaultPath: path.join(app.getPath('documents'), 'note.txt'),
filters: [{ name: 'Text', extensions: ['txt', 'md'] }],
})
if (canceled || !filePath) return false
await fs.writeFile(filePath, content, 'utf8')
return true
})
Notifications and secure storage
Notification in the main process posts to the system notification center; renderers can also use the web Notification API. On Windows, notifications need an Application User Model ID; installers usually set it, and in development you can call app.setAppUserModelId().
Encrypt sensitive strings such as passwords or access tokens with safeStorage, which uses the operating system's key storage, before writing them to disk.
const { Notification, safeStorage } = require('electron')
function notifyDone(fileName) {
if (Notification.isSupported()) {
new Notification({ title: 'Export complete', body: `Saved ${fileName}.` }).show()
}
}
async function saveToken(token) {
if (!safeStorage.isEncryptionAvailable()) throw new Error('encryption unavailable')
const encrypted = safeStorage.encryptString(token) // Buffer
await fs.writeFile(path.join(app.getPath('userData'), 'token.bin'), encrypted)
}
async function loadToken() {
const encrypted = await fs.readFile(path.join(app.getPath('userData'), 'token.bin'))
return safeStorage.decryptString(encrypted)
}
State in the renderer
A renderer handles state like any web app. Keep UI state in your framework's state tools (React, Vue, Svelte, and so on), small values in localStorage, and larger data in IndexedDB. With the default session these stores live under userData, so they survive restarts.
When several windows must see the same data, make the main process the single source of truth. Renderers request values over IPC, and the main process broadcasts changes to every window.
// preload.js
contextBridge.exposeInMainWorld('settings', {
get: () => ipcRenderer.invoke('settings:get'),
setTheme: (theme) => ipcRenderer.invoke('settings:setTheme', theme),
onChanged: (callback) => {
ipcRenderer.on('settings:changed', (_event, value) => callback(value))
},
})
// renderer.js
const apply = (settings) => { document.documentElement.dataset.theme = settings.theme }
window.settings.get().then(apply)
window.settings.onChanged(apply)
Summary
- Store app data under
app.getPath('userData'), never in the installation folder. - Access files in the main process and open only validated IPC handlers to renderers.
- Use
dialog, Notification, and safeStorage for dialogs, notifications, and encrypting sensitive values. - Keep UI state in the renderer and state shared across windows in the main process.
0 comments
Sign in · Sign in to leave a comment.