Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Reading and writing files

To keep notes after a restart, they must be written to disk. The main process reads and writes files with the node:fs/promises module from Node.js. What is left is to decide where the file goes: Electron has app.getPath for that.

The app data folder

app.getPath('userData') returns the folder the system gives the app for its data:

SystemPath
WindowsC:\Users\Anna\AppData\Roaming\notes
macOS~/Library/Application Support/notes
Linux~/.config/notes

The last part of the path is the name field from package.json (or productName if set). The folder is created automatically.

The file path

Add to the top of main.js:

// main.js
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('node:path');
const fs = require('node:fs/promises');
 
function notesFile() {
  return path.join(app.getPath('userData'), 'notes.json');
}

getPath works only after the app is ready, so the path is computed by a function at call time, not when the file is first run.

Reading

// main.js
async function loadNotes() {
  try {
    const json = await fs.readFile(notesFile(), 'utf8');
    return JSON.parse(json);
  } catch (error) {
    if (error.code === 'ENOENT') {
      return [];
    }
    throw error;
  }
}

On first launch the file does not exist yet — readFile throws an error with code ENOENT, and the function returns an empty list. Other errors are rethrown.

Writing

// main.js
async function saveNotes(notes) {
  await fs.writeFile(notesFile(), JSON.stringify(notes, null, 2), 'utf8');
}

JSON.stringify with an indent of 2 makes the file easy for a person to read.

IPC handlers

Connect the functions to the channels from the IPC lesson:

// main.js
app.whenReady().then(() => {
  ipcMain.handle('notes:load', () => loadNotes());
  ipcMain.handle('notes:save', (event, notes) => saveNotes(notes));
  createWindow();
});
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
 
contextBridge.exposeInMainWorld('notes', {
  load: () => ipcRenderer.invoke('notes:load'),
  save: (notes) => ipcRenderer.invoke('notes:save', notes),
});

A handler may return a promise — invoke on the page waits for it.

On the page

// renderer.js
let notes = [];
 
form.addEventListener('submit', async (event) => {
  event.preventDefault();
  notes.push(text.value);
  await window.notes.save(notes);
  text.value = '';
  render();
});
 
window.notes.load().then((saved) => {
  notes = saved;
  render();
});

The render function prints the notes array into the list, like showNotes in the previous lesson. Add a couple of notes and restart the app — they are still there. You can open notes.json in the app data folder.

Common mistake

Do not save data next to the code with path.join(__dirname, 'notes.json'). It works during development, but in a built app the code lives inside the app.asar archive or in Program Files, where writing is not allowed. Always keep user data in app.getPath('userData').

Thanks for reading!

Donate to the site — we work hard for you.