0 / 91
Week 4 · Day 27 of 91

Local storage and JSON

JavaScript Foundations II and the Browser

Objective

Persist simple browser data and understand serialization limits.

Events are interrupts, promises represent future results, and the DOM is the browser’s live model of the page.

  • JSON stringify and parse
  • localStorage keys
  • defensive loading

Why this matters

Your inventory app is honest and useless in exactly one way: press refresh and everything is gone. The array lives in memory, and memory belongs to the page, which the browser throws away on reload.

Today you make it survive. You will convert your array into text, store that text in the browser, read it back on the next visit, and — the part most tutorials skip — handle the case where what comes back is missing or damaged. By the end the app remembers your components across refreshes and across closing the browser entirely.

JSON: structure as text

Storage and networks move text and bytes, not JavaScript objects. To save a nested array of objects you must first flatten it into a string, and later rebuild it. That conversion is serialization, and the standard text format for it is JSON — JavaScript Object Notation, the format Day 1 mentioned as what servers send back.

JSON looks like JavaScript object syntax with stricter rules:

[{ "id": "R1", "name": "Resistor 10k", "quantity": 42, "inStock": true }]
  • Property names are always in double quotes. Single quotes are invalid.
  • Strings use double quotes. No trailing commas. No comments.
  • Allowed values: string, number, boolean, null, array, object. That is the whole list.

Two built-in functions do the conversion, and they are exact opposites:

const text = JSON.stringify(components);   // array  -> string
const back = JSON.parse(text);             // string -> array

JSON.stringify takes an optional third argument for indentation, which is what makes a file readable by a human:

console.log(JSON.stringify({ id: "C1", quantity: 8 }, null, 2));
{
  "id": "C1",
  "quantity": 8
}

You cannot put a struct on the wire

A serial link carries a byte stream. To send a struct you pack it into bytes at one end and unpack it at the other, and both ends must agree on the layout. Anything that is not data — function pointers, addresses that only mean something in this process — cannot survive the trip. JSON.stringify is the pack step, JSON.parse is the unpack, and the "allowed values" list above is the agreed layout.

What survives the round trip

Serialization is lossy, and knowing what it drops prevents a whole class of confusing bugs.

const record = {
  id: "C1",
  quantity: 8,
  inStock: true,
  discontinued: null,
  addedAt: new Date("2026-08-03T09:00:00Z"),
  supplier: undefined,
  reorder() { return "ordering"; },
};
console.log(JSON.stringify(record));
{"id":"C1","quantity":8,"inStock":true,"discontinued":null,"addedAt":"2026-08-03T09:00:00.000Z"}

Three things happened. supplier and reorder vanishedundefined values and functions are not JSON, so those keys are silently dropped. null survived, because null is a JSON value. And addedAt, a Date object, became a string. Parse it back and typeof parsed.addedAt is "string", not an object with .getFullYear() on it. Nothing warns you; the crash arrives later.

The rule: store plain data. If you need a date back as a Date, rebuild it yourself with new Date(parsed.addedAt) after parsing.

localStorage: keys and strings

localStorage is a small key–value store built into every browser. It survives reloads, tab closes, and browser restarts, and it has three methods you need:

localStorage.setItem("inventory:components", text);
const raw = localStorage.getItem("inventory:components");
localStorage.removeItem("inventory:components");

Four facts that decide how you use it:

  • Everything is a string. localStorage.setItem("count", 42) stores the text "42", and getItem gives you "42" back. This is exactly why JSON.stringify comes first: an array is not a string, so the browser converts it with the default rules and you get useless text like [object Object],[object Object].
  • getItem returns null when the key does not exist. First visit, cleared storage, renamed key — all produce null, and JSON.parse(null) does not do what you want.
  • Storage is per origin. Everything served from http://localhost:3000 shares one store, so every project you serve on that port sees the same keys. Namespace them: inventory:components, not data.
  • It is roughly 5 MB and completely visible. The user can read and edit every value in DevTools.

localStorage is not a safe place for secrets

Anything stored here is plain text, readable and editable by the user and by any script running on the page. Never put passwords, API keys, or personal data in it. It is for convenience state — a draft, a preference, a local inventory — and nothing that matters if it is tampered with. Day 51 covers where credentials actually belong.

Look at the store yourself

On your served page, open DevTools → ApplicationLocal Storage → your localhost origin. It is a table of keys and values, and you can edit a value in place. Run localStorage.setItem("test", "hello") in the Console and watch the row appear.

Defensive loading

Saving is easy. Loading is where apps break, because the stored value can be in three bad states: missing, unparseable, or parseable but the wrong shape. All three happen in real use — a first visit, a value someone edited by hand, or data written by an older version of your own code.

JSON.parse on invalid text throws, which on Day 24 you learned stops the program:

JSON.parse("not json");
SyntaxError: Unexpected token 'o', "not json" is not valid JSON

To survive that, wrap it in try/catch. The try block runs the risky code; if anything inside it throws, execution jumps straight to catch with the error object, and the program continues instead of stopping.

const STORAGE_KEY = "inventory:components:v1";

function isValidComponent(item) {
  return (
    item !== null &&
    typeof item === "object" &&
    typeof item.id === "string" &&
    typeof item.name === "string" &&
    Number.isInteger(item.quantity)
  );
}

function loadComponents() {
  const raw = localStorage.getItem(STORAGE_KEY);
  if (raw === null) return [];                    // first visit: not an error
  try {
    const parsed = JSON.parse(raw);
    if (!Array.isArray(parsed)) return [];        // right text, wrong shape
    return parsed.filter(isValidComponent);       // drop damaged records
  } catch (error) {
    console.warn("Stored inventory was unreadable; starting empty.", error);
    return [];
  }
}

Every branch returns a usable array, so the rest of the app never has to ask whether loading worked. That is the whole point of defensive loading: absorb the mess at the boundary and hand clean data inward. The :v1 in the key is cheap insurance — change your object shape later and you can move to :v2 without tripping over old records.

Reading someone else's handwriting

A form arrives from outside your office. It might be blank, it might be soaked and unreadable, or it might be filled in on last year's form with fields you no longer use. A clerk who assumes every form is perfect jams the whole queue on the first bad one. A clerk who checks each form and sets aside the unusable ones keeps working. Stored data is a form from outside — even when you wrote it yourself last week.

Walkthrough

Add persistence to yesterday's app. Put STORAGE_KEY, isValidComponent, and loadComponents above your other code, then add saving and start-up loading:

function saveComponents() {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(components));
}

Call it wherever state changes — the two functions that already exist for exactly that reason:

function addComponent(item) {
  components.push(item);
  saveComponents();
  render();
}

function deleteComponent(id) {
  components = components.filter((item) => item.id !== id);
  saveComponents();
  render();
}

And replace the initial let components = [] and the bare render() at the bottom:

let components = loadComponents();
render();

Serve the page, add two components, and press refresh. They are still there. Open Application → Local Storage and you will see one row: the key inventory:components:v1 and a value that is your array as JSON text.

Now break it on purpose. In the Console:

localStorage.setItem("inventory:components:v1", "not json");
location.reload();

The page loads with an empty list and a warning in the Console — no crash, no blank screen. That is catch doing its job. Now try the subtler damage: set the value to '{"a":1}' and reload. It parses fine but is not an array, so Array.isArray sends you to an empty list.

Finally, a reset the user can reach:

document.querySelector("#reset-button").addEventListener("click", () => {
  if (!confirm("Delete all components? This cannot be undone.")) return;
  localStorage.removeItem(STORAGE_KEY);
  components = [];
  render();
});

confirm shows a browser dialog and returns true or false. Removing the key, rather than storing "[]", leaves storage genuinely clean.

Checkpoint

Add a component, close the tab completely, reopen the served address. It is still there. Then explain why JSON.stringify is needed before setItem — in one sentence, about strings.

Your turn

Build the deliverable: inventory persists across refresh and can reset safely.

  1. Add loadComponents, saveComponents, and the STORAGE_KEY to your app. Add three components and refresh. All three survive.
  2. Inspect the stored value in Application → Local Storage. Confirm it is one JSON string containing every field you expect, with double-quoted property names.
  3. Add the #reset-button and its listener. Reset, refresh, and confirm the list is still empty and the key is gone from the storage table.
  4. Corrupt the value by hand in the DevTools table — delete a closing brace — and reload. Record the console warning. The app must still open.
  5. Store one object with a quantity of "12" (a string, not a number) by editing storage directly, then reload. isValidComponent should drop it. Confirm it does not appear.
  6. Add an addedAt: new Date().toISOString() field to new components. Confirm after a reload that it comes back as a string, and format it for display with new Date(item.addedAt).

You are done when

Components survive a refresh and a browser restart, reset clears them permanently, and the app still opens cleanly when the stored value is missing, corrupt, or the wrong shape.

Pair mode — after your loader handles the three bad cases

Today's mode is pair: take one small task at a time, and before accepting an AI-generated change, inspect the diff, run your checks, and understand the behaviour it produces.

"Review my storage loading code for parse failures and invalid shapes."

Test every case it raises by writing that bad value into storage yourself and reloading. A suggestion you have not reproduced is not yet a finding.

Common pitfalls

  • Storing the array without stringify. setItem(key, components) stores text like [object Object],[object Object]. Refresh and everything is gone, with no error to explain it.
  • JSON.parse(localStorage.getItem(key)) with no null check. On a first visit that is JSON.parse(null), which returns null rather than throwing — so your components becomes null and the next .filter fails with a TypeError.
  • Saving in render. It is tempting and it is wrong: render is for drawing, and drawing should never write. Save where the state changes.
  • Expecting Date objects back. They return as strings. Rebuild with new Date(...) if you need date methods.

Verify it yourself

Open today's reference, MDN's Dynamic scripting with JavaScript, and find its pages on JSON and on client-side storage.

  1. MDN documents sessionStorage next to localStorage. Find it and write one sentence on how its lifetime differs, and one situation where you would prefer it.
  2. Find MDN's note on the storage quota. What error does exceeding it produce, and what does that mean for an app that saves on every keystroke?

Add both answers to notes/day-27.md. Tomorrow you assemble the whole week, and knowing the limits of your storage is part of knowing when your app would fail.

The hour

  1. 0–5 min Recall

    Without notes, state yesterday’s main idea and one unresolved question.

  2. 5–20 min Learn

    Read only the listed concept notes and official reference sections needed today.

  3. 20–48 min Build

    Save inventory data, reload the page, and recover it. Handle missing or malformed data.

  4. 48–55 min Explain and verify

    Run the result, inspect evidence, and explain the data/control flow in your own words.

  5. 55–60 min Quiz and commit

    Complete the quiz, record one lesson, and commit the verified change when applicable.

What to hand in

Deliverable

Inventory persists across refresh and can reset safely.

Working with AI today

AI as pair programmer

Define one small task, review the plan, inspect the diff, run checks, and explain every changed section.

Review my storage loading code for parse failures and invalid shapes.

References

End-of-day quiz

Q1 Why use JSON.stringify before localStorage?
Q2 Which result best proves today’s work is complete?
Q3 Before accepting an AI-generated code change, what should you do?

Explain-back gate

Pass the quiz above to unlock completion.

Quiz + explain-back checks required.