Windows and the renderer

Render notes safely

Keep note data in renderer state and draw user text without turning it into executable HTML.

The renderer state is ordinary browser JavaScript. No framework needed for an app this size. Let’s start src/renderer.js with the state and a few element references:

let notes = []
let selectedId = null

const list = document.querySelector('#notes')
const titleInput = document.querySelector('#title')
const bodyInput = document.querySelector('#body')

notes holds every note as a plain object with id, title, and body. selectedId remembers which one is open in the editor.

Draw the list with textContent

Now the function that draws the list. We clear it, then create one button per note:

function renderNotes() {
  list.replaceChildren()

  for (const note of notes) {
    const button = document.createElement('button')
    button.type = 'button'
    button.textContent = note.title || 'Untitled'
    button.addEventListener('click', () => selectNote(note.id))

    const item = document.createElement('li')
    item.append(button)
    list.append(item)
  }
}

The important line is button.textContent = note.title. Notes are user data, not markup. textContent puts the string on screen as text, no matter what it contains.

The alternative many people reach for is building an HTML string and assigning it to innerHTML. Don’t do that with user data. A note titled <img src=x onerror=alert(1)> would become a real image tag, and its onerror would run. In a browser tab that’s an XSS bug. In Electron it’s a script running inside your desktop app.

Creating DOM nodes also keeps things tidy. Each button gets its own listener, and there is no escaping logic to get wrong.

Use stable IDs

Notice the click handler passes note.id, not the array index. An index changes every time a note is inserted or deleted. If the user clicks the third item while a save reorders the list, an index points at the wrong note. An ID always points at the same one.

Test with hostile content now

Don’t wait for a bug report. Put a nasty note in the state and render it:

notes = [{
  id: 'test-note',
  title: '<img src=x onerror=alert(1)>',
  body: '<script>alert(1)</script>'
}]

Call renderNotes() and look at the window. You should see the raw tags as visible text in the list. No alert, no network request in the DevTools Network tab, no script execution.

Keep this case in your renderer checklist. A refactor that switches to a template string is easy to make months later, and this test catches it in seconds.

One last thing. Rendering safely and validating data are two separate jobs. textContent protects the screen. It does not check that a note has the right shape or a reasonable size. The main process still validates every note before it touches the disk, and we build that validator in a few lessons.

Lesson completed