Learn by reading through in order

localStorage — Saving Data in the Browser

Save values in the browser with localStorage's setItem, getItem, and removeItem, store arrays as JSON strings, and handle null and the size limit.

If you keep a user's cart items or display settings in variables, they're lost when the page is reloaded or closed, and everything goes back to its initial state the next time it opens. None of the code so far has had a place to keep values across page loads.

This article covers localStorage, which saves values in the browser, and the JSON strings you use to save arrays and objects.

Keeping Values After the Page Closes — setItem and getItem

Suppose a news site wants to remember the font size a user chose and use it again on their next visit. Even if you put it in a variable, as in const fontSize = "large";, variables are lost when the page is reloaded or closed, so the next page load can't read it.

localStorage (a key-value store that the browser provides) saves a value with setItem(key, value), reads it back with getItem(key), and removes it with removeItem(key).

Storage is separated by origin (the part of a URL before the path — https://news.example in https://news.example/settings), and pages from the same origin read and write the same values.

// If a value from before is still saved, remove it so we start with nothing saved
localStorage.removeItem("fontSize");

// Save when the user switches the font size to large
localStorage.setItem("fontSize", "large");

// When the page opens, read the saved value and use it
const fontSize = localStorage.getItem("fontSize");
console.log(fontSize);                            // large

// Saving under the same key replaces the previous value
localStorage.setItem("fontSize", "small");
console.log(localStorage.getItem("fontSize"));    // small
Storage Is Separate for Each Origin
Origin https://news.example
  • Pages and tabs from this origin share one localStorage
localStorage — key-value pairs
  • "fontSize""small" (replaced by saving to the same key)
  • Stays after the page is closed or reloaded
The open page
  • Variables like const fontSize
  • Variables are re-created on reload
Origin https://shop.example
  • Has its own localStorage
  • Can't read the "fontSize" saved by news.example
Variables are re-created on reload, but localStorage values remain. Pages from a different origin can't read them, even with the same key.

If you read with getItem when the page opens and write with setItem when the user changes the setting, the page shows the same font size even after a reload. Saved values stay until the page's code removes them or the user clears them in the browser settings.

Saved Values Carry Over Between Exercise Runs

The exercise console runs every JavaScript exercise on this site on the same origin, so they all share one localStorage, and its contents stay even if you reopen the page. To keep values saved in earlier runs from mixing into your results, the exercises start by removing the keys they use with removeItem.

In a map app, save the map type and the distance unit under separate keys. The unit from last time is already saved, and the selected type, selectedMapType, is already declared.

① Save the selected map type.

② Read the saved distance unit into a variable.

③ Overwrite just the distance unit with "mile".

④ Print the variable from ② and the values of the two keys as "km / mile / satellite".

(If it runs correctly, an explanation will appear.)

JavaScript / TypeScript Editor

Run code to see output

Saving Arrays and Objects — JSON.stringify

A travel booking site keeps the hotels a user has added to their favorites in an array and wants to list them again on the next visit. Passing the array straight to setItem doesn't cause an error, but the value you read back contains neither the hotel IDs nor their names.

setItem converts the value you pass to a string before saving it. Turn arrays and objects into JSON strings with JSON.stringify before saving them, and turn the string you read back into the original array or object with JSON.parse.

// If a value from before is still saved, remove it first
localStorage.removeItem("favoriteHotels");
const favoriteHotels = [{ id: "H-101", name: "Harbor Hotel" }, { id: "H-204", name: "Station Inn" }];

// Passed directly, the array is converted to a string when it's saved
localStorage.setItem("favoriteHotels", favoriteHotels);
console.log(localStorage.getItem("favoriteHotels"));   // [object Object],[object Object]

// Convert it to a JSON string before saving
localStorage.setItem("favoriteHotels", JSON.stringify(favoriteHotels));
const saved = localStorage.getItem("favoriteHotels");
console.log(saved);                                    // [{"id":"H-101","name":"Harbor Hotel"},{"id":"H-204","name":"Station Inn"}]

// Turn the string you read back into an array before using it
const restored = JSON.parse(saved);
console.log(restored[1].name);                         // Station Inn
The Contents Are Lost at the Moment of Saving
Pass the raw arrayto setItem[object Object],…is storedgetItem returnsthe same stringThe hotel namesare goneJSON.stringify,then setItem[{"id":"H-101",…is storedgetItem, thenJSON.parserestored[1].nameis Station Inn
An array passed directly turns into the string [object Object] when it's saved. After getItem, there's no way to get the original array back.

Numbers and booleans are converted silently too, with no error, so you only notice when a calculation or an if check on the value you read back goes wrong. The table below shows, for each value passed to setItem, the string that's saved and what happens when you read it back.

Value Passed to setItemStored StringWhen You Read It Back
The number 20"20""20" + 1 is "201" (use Number)
The boolean false"false"Truthy in if (use JSON.parse)
String array ["A-1", "B-2"]"A-1,B-2"One comma-joined string, not an array

In a vocabulary app, add one word to the list of words to review. The list is already saved under the key reviewWords, and the word to add, newWord, is already declared.

① Read the saved list and print the type of the value.

② Turn the value you read back into an array and add newWord to the end.

③ Save the updated array again under the same key.

④ Read it again, turn it back into an array, and print the number of words and the word of the last one.

JavaScript / TypeScript Editor

Run code to see output

Reading a Key That Isn't Saved — the null from getItem

On a recipe site, the IDs of viewed recipes are saved as a history, and the page shows how many there are when it opens. First-time visitors and people who cleared their history have nothing saved, so if you read the length of the JSON.parse result assuming a history exists, that line throws a TypeError.

When you pass getItem a key that isn't saved, it returns null, not undefined. If you write JSON.parse(localStorage.getItem(key)) ?? [], an empty array is used only when nothing is saved.

// Read the view history. If nothing is saved, use an empty array
function loadHistory() {
  return JSON.parse(localStorage.getItem("viewHistory")) ?? [];
}

// First visit — nothing is saved, so getItem returns null
localStorage.removeItem("viewHistory");
console.log(localStorage.getItem("viewHistory"));   // null
console.log(loadHistory().length);                  // 0

// With a saved history — the string is turned back into an array
localStorage.setItem("viewHistory", JSON.stringify(["R-12", "R-30"]));
console.log(loadHistory().length);                  // 2

// After removeItem — it's null again
localStorage.removeItem("viewHistory");
console.log(loadHistory().length);                  // 0
With Nothing Saved, parse Returns null Too
Nothing saved(new or removed)getItemreturns nullJSON.parse(null)returns null too?? gives [], solength is 0Saved["R-12","R-30"]getItem returnsa JSON stringJSON.parse givesa 2-item array?? isn't used;length is 2
JSON.parse doesn't treat the null returned by getItem as an error; it just returns null. Adding ?? [] lets you treat it as an array.

If you put the reading code in a function, callers can treat the result as an array without checking for null, even after removeItem is called on logout. removeItem doesn't throw an error even for a key that isn't saved, so you don't need to check whether something is saved before removing it.

In a daily report app, keep unfinished reports as drafts by date. Only the draft for the 10th is saved, and an empty loadDraft function is already declared.

① Make loadDraft return the draft for a date, or an empty string if there isn't one.

② Read the key for the 11th with getItem and print the return value.

③ Use loadDraft to print the lengths of the drafts for the 10th and the 11th.

④ Remove the draft for the 10th and print its length using loadDraft.

JavaScript / TypeScript Editor

Run code to see output

Restoring the Cart After a Reload — Save and Load Functions

Next, an online store's cart should keep its contents when the page is reloaded after an item is added. Adding an item with cart.push only changes the variable, so if you miss the save line anywhere, that item disappears on reload.

Put saving in a saveCart function and loading in a loadCart function, and call them right after the cart changes and when the page opens. setItem is synchronous (it finishes saving before moving on to the next line), so unlike fetch, you can read the saved value on the next line without await.

localStorage.removeItem("cart");   // If a cart from before is still saved, remove it
// Save the cart as a JSON string. No await needed
function saveCart(cart) {
  localStorage.setItem("cart", JSON.stringify(cart));
}
// Turn the saved cart back into an array. If nothing is saved, return an empty cart
function loadCart() {
  return JSON.parse(localStorage.getItem("cart")) ?? [];
}

// Load when the page opens, and save after adding an item
const cart = loadCart();
cart.push({ id: "K-310", qty: 1 });
saveCart(cart);

// Instead of reloading, load again from the saved string without using the cart variable
const reloaded = loadCart();
console.log(reloaded.length, reloaded[0].id);     // 1 K-310
Forget saveCart and Nothing Comes Back
Add K-310with pushCallsaveCart(cart)Reload, thenloadCart()reloaded.lengthis 1Add K-310with pushForget to callsaveCartReload, thenloadCart()reloaded.lengthis 0
Without a saveCart call, the pushed K-310 only exists in the cart variable. After a reload, what comes back is whatever was saved last.

If you put everything that changes the cart in one function and call saveCart at its end, you don't have to write the save at every call site, and you can't forget it. Loading is also wrapped in loadCart, so there's no place where you could forget ?? [] either.

setItem Throws an Exception When Storage Is Full

In Chrome, one origin can store about 5.24 million characters, counting keys and values together. Beyond that, setItem throws a QuotaExceededError and the value isn't saved. Reads and writes are synchronous, so the page also stops responding while a large value is being saved. Use it for small values like carts and settings.

On a production line's inspection screen, save the defects found, with a count for each part number. saveDefects, loadDefects, and an empty addDefect function are already declared.

① In addDefect, read the saved records and look for a record with the same part number.

② If one is found, add 1 to its count; if not, add a record with a count of 1. Then save.

③ Record defects for W-12, W-40, and W-12, in that order.

④ Read the records again with loadDefects and print each part number in the form "W-40: 1".

JavaScript / TypeScript Editor

Run code to see output
QUIZ

Knowledge Check

Answer each question one by one.

Q1What happens when you run localStorage.getItem("theme") with a key that has never been saved?

Q2After localStorage.setItem("darkMode", false), what happens if you use the value you read back as an if condition?

Q3If you reload after cart.push(book) without calling saveCart, what does loadCart() return?