Students predict a visit counter after three runs, save a reading list as JSON so it survives Run, find a list saved without JSON.stringify, make Start again forget the saved copy, remember a name, and keep a best score.
Load these into Waypoints Prepare: choose Load URL and paste the copied address, or download the file and import it. In Prepare, the worksheet's Teacher edition shows the answers; the student copy hides them.
Predict and Run 10 min · Investigate 15 min · Modify 10 min · Make 15 min
An optional lesson at the end of the DOM course, and the "saving" step for every route through the Web pathway. localStorage is a small store each browser keeps per website: setItem(name, text), getItem(name) (null when nothing is saved) and removeItem(name). It keeps text only, so lists and objects go through JSON.stringify and JSON.parse (met in the fetch lesson). The planted bug saves a list without stringify: the first run looks fine, the second fails, because the bad text is still saved. The playground's hint says to clear it.
The pattern for any app: load once when the page opens, save every time the data changes. In the playground, each Run is a new visit; the playground keeps each step's saved data for this browser (the preview page cannot use the real store, so a stand-in behaves the same way). On a real site it is the browser's own localStorage, per site and per browser: nothing is shared between computers or people, and clearing browsing data deletes it. Don't save anything private.
For Vue students (the to-do app): load the list in setup (const tasks = ref(JSON.parse(localStorage.getItem("tasks")) || [])), then watch(tasks, (t) => localStorage.setItem("tasks", JSON.stringify(t)), { deep: true }). Preact and Svelte have their own versions (useEffect, $effect).
Students forget that saving is separate from the page: they change the list and expect it saved. Ask "which line saves it?" after every change they make.
| What you will see | A question to ask | The fix |
|---|---|---|
| Red error: is not valid JSON. | Was the list saved with JSON.stringify? | Save with JSON.stringify; the bad copy is still saved, so remove it once (localStorage.removeItem) and run again. |
| The data is gone after Run. | Does the code save it when it changes, and load it at the start? | setItem after every change; getItem when the page opens. |
| A saved number adds like text (1 + 1 = 11). | What does getItem give back? | Text: turn it into a number with Number(...). |
One possible solution for each task. Other correct programs are fine.
<h1>Books I have read</h1>
<input id="title" placeholder="A book">
<button id="add">Add</button>
<ul id="books"></ul>
<button id="clear">Start again</button>
<script>
const list = document.querySelector("#books");
let books = [];
const saved = localStorage.getItem("books");
if (saved !== null) {
books = JSON.parse(saved);
}
function show() {
list.textContent = "";
for (const title of books) {
const li = document.createElement("li");
li.textContent = title;
list.append(li);
}
}
document.querySelector("#add").addEventListener("click", function () {
const box = document.querySelector("#title");
books.push(box.value);
localStorage.setItem("books", JSON.stringify(books));
box.value = "";
show();
});
document.querySelector("#clear").addEventListener("click", function () {
books = [];
localStorage.removeItem("books");
show();
});
show();
</script>
<h1>Books I have read</h1>
<input id="title" placeholder="A book">
<button id="add">Add</button>
<ul id="books"></ul>
<button id="clear">Start again</button>
<p>Reader: <input id="reader"></p>
<script>
const list = document.querySelector("#books");
let books = [];
const saved = localStorage.getItem("books");
if (saved !== null) {
books = JSON.parse(saved);
}
function show() {
list.textContent = "";
for (const title of books) {
const li = document.createElement("li");
li.textContent = title;
list.append(li);
}
}
document.querySelector("#add").addEventListener("click", function () {
const box = document.querySelector("#title");
books.push(box.value);
localStorage.setItem("books", JSON.stringify(books));
box.value = "";
show();
});
document.querySelector("#clear").addEventListener("click", function () {
books = [];
show();
});
const reader = document.querySelector("#reader");
if (localStorage.getItem("reader") !== null) {
reader.value = localStorage.getItem("reader");
}
reader.addEventListener("input", function () {
localStorage.setItem("reader", reader.value);
});
show();
</script>
<p>Clicks: <span id="count">0</span> · Best: <span id="best">0</span></p>
<button id="click">Click me</button>
<button id="new">New game</button>
<script>
// Save the best score, so it is still there after Run (or a visit next week).
let clicks = 0;
let best = Number(localStorage.getItem("best"));
const countEl = document.querySelector("#count");
const bestEl = document.querySelector("#best");
bestEl.textContent = best;
document.querySelector("#click").addEventListener("click", function () {
clicks = clicks + 1;
countEl.textContent = clicks;
if (clicks > best) {
best = clicks;
bestEl.textContent = best;
localStorage.setItem("best", best);
}
});
document.querySelector("#new").addEventListener("click", function () {
clicks = 0;
countEl.textContent = clicks;
});
</script>
Stretch: Save the to-do app from Apps with Vue (see the note above). Save a whole object (name and score) as one JSON item.
Support: Give the load and save lines on the board; students choose where each goes.
Why must a list be saved with JSON.stringify?
localStorage keeps only text; JSON.stringify turns the list into text that JSON.parse can turn back into a list.
Next on the Web pathway: Your site, for real (accessibility, then putting the page online). Sharing saved data between people needs a server: Web apps with Flask.