Web Storage

Save a small preference

Store and restore one replaceable string preference with localStorage without turning it into an application database.

localStorage is the right home for a small preference that should survive a browser restart. The theme in our field-notes app is the perfect example.

Write it, then read it at startup

The API is tiny. setItem() stores a string under a key, getItem() reads it back. When the key doesn’t exist you get null, so always provide a default.

We write the preference when the user changes it, and read it once while the page starts:

localStorage.setItem('field-notes:theme', 'dark')

const theme = localStorage.getItem('field-notes:theme') ?? 'light'
document.documentElement.dataset.theme = theme

After the first line, open the Application panel and you’ll see the row field-notes:theme with value dark. Reload the page and the second block picks it up. Close the browser, reopen it, and it’s still there. That persistence is the reason we chose localStorage over sessionStorage.

Prefix your keys

Notice the key is field-notes:theme, not theme. Every script on the origin shares the same localStorage. An analytics snippet, a chat widget, or an older version of your own app could all decide theme is a good key name. Then they overwrite each other and you spend an afternoon wondering why the theme flips randomly.

An app:feature prefix costs nothing and avoids the collision. I use it on every key.

Keep the startup read small

Web Storage is synchronous. While getItem() runs, nothing else on the page runs. For one short string that’s a few microseconds and nobody notices.

The trouble starts when the “preference” grows into a database. Reading a 2 MB JSON string at startup blocks rendering for a visible moment. If you find yourself storing a list that grows, that’s your signal to move it to IndexedDB, which we cover in the next module.

Treat the value as input

localStorage is writable by anyone with access to the origin: your code, an extension, or the user in DevTools. So the string you read back is input, not trusted configuration.

Only apply values you recognize:

const saved = localStorage.getItem('field-notes:theme')
const theme = ['light', 'dark'].includes(saved) ? saved : 'light'

Without this check, a value like dark"><script> ends up in a data-theme attribute. It won’t execute there, but an unknown theme name still leaves the page unstyled, and the habit of validating protects you in places where it matters more.

Prove it works

Save a theme, reload, and confirm it comes back. Then remove the key with localStorage.removeItem('field-notes:theme') and reload again. The page must render in the default theme without throwing. Finally edit the value in DevTools to something like purple and check the page still falls back to light. Three reloads, and you’ve tested the happy path, the missing key, and the invalid value.

Lesson completed