Marcy Lab School Docs
  • Welcome
  • Student Guidelines & Policies
    • Student Handbook
    • AI Policy
    • Academic Calendar
  • Environment Setup
    • Local Environment Setup - Mac
    • Local Environment Setup - Windows
    • GitHub Setup
    • Postgres Setup
  • How-Tos
    • How To Code at Marcy: Code Style Guide
    • How to Do Short Response and Coding Assignments
    • How to Debug
    • How to PEDAC
    • How to Create A GitHub Organization and Scrumboard
    • How to Create Projects with Vite
    • How to Deploy on GitHub Pages
    • How to Deploy on Render
    • How to Test your API with Postman
  • Mod 0 - Command Line Interfaces, Git, and GitHub
    • Overview
    • 1. Command Line Interfaces
    • 2. Git & GitHub
    • 3. Git Pulling & Merging
    • 4. Git Branching & PRs
  • Mod 1 - JavaScript Fundamentals
    • Overview
    • 1. Intro to Programming
    • 2. Errors
    • 3. Node & Node Modules
    • 4. Variables, Functions & String Methods
    • 5. Control Flow, typeof, and Math
    • 6. Loops
    • 7. Arrays
    • 8. Objects
    • 9. Higher Order Functions: Callbacks
    • 10. Higher Order Functions: Array Methods
    • 11. Regex
  • Mod 2 - HTML, CSS & the DOM
    • Overview
    • 1. HTML
    • 2. CSS
    • 3. Accessibility (a11y)
    • 4. The Document Object Model (DOM) API
    • 5. Events
    • 6. Forms
    • 7. The Box Model and Positioning
    • 8. Flexbox
    • 9. Grid & Media Queries
    • 10. ESModules
    • 11. Vite
    • 12. LocalStorage
  • Mod 3 - Async & APIs
    • Overview
    • 1. Promises
    • 2. Fetch
    • 3. Building a Fetching App
    • 4. Async & Await
    • 5. A Generic Fetch Handler
  • Mod 4 - Project Week!
    • Important How Tos and Guides
      • How to Create a GitHub Organization and Scrum Board
      • How To Start a Project with Vite
      • How To Deploy a Project with GitHub Pages
    • Project Week Overview
    • Agile Methodologies
    • Deliverables & Milestones
    • Technical Requirements Checklist
    • Free API List
    • Collaborative GitHub
  • Mod 5 - Object-Oriented Programming
    • Overview
    • 1. Intro to OOP, Encapsulation, Factory Functions, and Closure
    • 2. Classes
    • 3. Private & Static
    • 4. UML Diagrams & Has Many/Belongs To Relationships
    • 5. Challenge: Implementing Has Many/Belongs To
    • 6. Inheritance
    • 7. Polymorphism
    • 8. Review and Practice
    • MDN: Object Prototypes
  • Mod 6 - Data Structures & Algorithms
    • Overview
    • Important How Tos and Guides
      • How to Debug
      • How to PEDAC
    • 1. Nodes & Linked Lists
    • 2. Singly & Doubly Linked Lists
    • 3. Stacks & Queues
    • 4. Recursion
    • 5. Trees
  • Mod 7 - React
    • Overview
    • Important How Tos and Guides
      • How to Create Projects with Vite
      • How to Deploy on GitHub Pages
    • 1. Intro to React
    • 2. Events, State, and Forms
    • 3. Fetching with useEffect
    • 4. React Router
    • 5. Building a Flashcards App
    • 6. React Context
    • 7. Global Context Pattern
  • Mod 8 - Backend
    • Overview
    • Important How Tos and Guides
      • How to Deploy on Render
      • How to Test your API with Postman
      • Postgres Setup
    • 1. Intro to Express
    • 2. Building a Static Web Server with Middleware
    • 3. Securing API Keys and Environment Variables
    • 4. RESTful CRUD API
    • 5. Model-View-Controller Architecture
    • 6. SQL and Databases
    • 7. JOIN (Association) SQL Queries
    • 8. Knex
    • 9. Your First Fullstack App!
    • 10. Migrations & Seeds
    • 11. Schema Design & Normalization
    • 12. Hashing Passwords with Bcrypt
    • 13. React Express Auth Template Overview
  • Mod 9 - Civic Tech Hackathon
    • Overview
    • Rubric
  • Mod 10 - Capstone
    • Overview
Powered by GitBook
On this page
  • Key Terms
  • Lets Make a Leaderboard!
  • localStorage Lets Us Store Data That Persists
  • JSON.stringify() and JSON.parse()
  • Adding the leaderboard to LocalStorage
  • localStorage Helpers
  • Data Layer: Creating an API for localStorage
  • Removing Values
  1. Mod 2 - HTML, CSS & the DOM

12. LocalStorage

Previous10. ESModulesNextMod 3 - Async & APIs

Last updated 5 months ago

Follow along with code examples !

Table of Contents

Key Terms

  • localStorage — an API for storing persistent data in a user's browser.

    • localStorage.setItem(key, value) — stores the given key:value pair in localStorage. The value is converted to a string using the value's .toString() method

    • localStorage.getItem(key) — retrieves the value associated with the given key in localStorage. It will always return a string.

    • localStorage.removeItem(key) — removes the value associated with the given key from localStorage.

    • localStorage.clear() — removes all key:value pairs from localStorage

  • .toString() — converts the source value or object to a string. How the string is converted varies depending on the source value or object.

  • JSON.stringify(value) — converts the given value to a string in the JSON format and returns it.

  • JSON.parse(jsonStr) — parses (inspects) a given JSON string, constructing and returning the JavaScript value or object described by the string. If the given string is not valid JSON, a SyntaxError will be thrown.

Lets Make a Leaderboard!

Making video games is one of the greatest joys of programming. And for good reason! Not only are they fun to play, but they are rewarding to create. Video games also can teach us important lessons about programming.

Suppose we had a game where the user is challenged to click a button as many times as they can in 5 seconds.

To encourage the competitive spirit of gaming, a key feature that we want to implement is a leaderboard to display the user's previous scores. Take a look at the speed-clicker-game where we've implemented this game!

Q: Look at main.js and see how the leaderboard is stored "in memory". What are the issues with storing a leaderboard in memory?

When the user refreshes the page or closes their browser, the program will end and all "in memory" variables will be thrown away. When we return to the page, a new leaderboard will be created.

localStorage Lets Us Store Data That Persists

The localStorage API allows you to store data in a user's browser that will persist across browser sessions. That is, the data from a previous session can be retrieved even after they close their browser.

localStorage is a globally available Object with the following methods:

  • localStorage.setItem(key, value) — stores the given key:value pair in localStorage. The value is converted to a string using the value's .toString() method

  • localStorage.getItem(key) — retrieves the value associated with the given key in localStorage. It will always return a string.

You can play around with these functions in your browser's Console to get a sense of how they work:

// localStorage.setItem(key, value)
localStorage.setItem('secret', 'hello world!');

// localStorage.getItem(key) — always returns a string
localStorage.getItem('secret'); // returns the string 'hello world!'

// let's try storing a leaderboard array!
localStorage.setItem('leaderboard', [10, 20, 30]);
localStorage.getItem('leaderboard'); // returns '10,20, 30'

Notice anything weird about the 'leaderboard' array that we stored in localStorage? It was converted to a string!

Let's try to add a new score to the leaderboard array:

// first get the existing leaderboard
const leaderboard = localStorage.getItem('leaderboard');

// try to push a new score into the array
leaderboard.push(40);

// Uncaught TypeError: leaderboard.push is not a function
Q: Why did we get a TypeError?

Because localStorage.setItem converted the given array into a string. When we try to retrieve that array, it is no longer an array!

JSON.stringify() and JSON.parse()

When using setItem, the value is turned into a string before being stored. It does this using the given value's .toString() method (every data type has this method).

As you can see, this is particularly annoying when dealing with Arrays and Objects. Try copying and pasting these statements into your browser Console:

const nums = [1, 2, 3];
const user = { name: 'ben' };

// toString will convert arrays and objects into strings
nums.toString() // '1,2,3'  <-- mildly annoying that the [] are gone
user.toString() // '[object Object]'  <-- what is this garbage??

localStorage.setItem('nums', nums);
localStorage.setItem('user', user);

localStorage.getItem('nums'); // '1,2,3' 
localStorage.getItem('user'); // '[object Object]'

When we want to store Arrays and Objects in localStorage, we often use JSON.stringify() to convert the string into a JSON-formatted string.

const startingLeaderboard = [10, 20, 30];

// JSON.stringify() turns a value into a JSON-formatted string, preserving their structure (still a string)
JSON.stringify(startingLeaderboard); // '[1, 2, 3]'

// stringify values before storing them in localStorage
localStorage.setItem('leaderboard', JSON.stringify(startingLeaderboard));

When we want to get the value out of localStorage, we use JSON.parse() which will create a JavaScript value / object from a JSON-formatted string.

// The item will be stored as a JSON string
localStorage.getItem('leaderboard'); // returns '[10,20,30]'

// JSON.parse it to get an Array back
JSON.parse(localStorage.getItem('leaderboard')); // returns [10,20,30]

In summary, when working with arrays and objects in localStorage, any time we need to insert a value into localStorage we will first stringify it with JSON.stringify and any time we want to get a value from localStorage, we will use JSON.parse():

// First get the value from localStorage to make an "in memory" copy
const leaderboard = JSON.parse(localStorage.getItem('leaderboard'));

// We can modify the copy
leaderboard.push(40);

// And then stringify it before updating the item in localStorage
localStorage.setItem('leaderboard', JSON.stringify(leaderboard));

// We can check out work after!
localStorage.getItem('leaderboard');

For now, lets empty out the localStorage:

localStorage.clear();

Adding the leaderboard to LocalStorage

Let's try to use localStorage to implement our leaderboard!

Previously, we had initialized leaderboard as an empty array "in memory" and then whenever the game ended, we would push the current score into that array.

To use localStorage, we'll follow a common pattern:

  1. Check to see if there is a leaderboard value in localStorage and store it in memory.

  2. If there isn't any previously-stored leaderboard, make a new array in memory.

  3. Modify the in-memory array with our new data.

  4. Store the in-memory array in localStorage, replacing the old value.

// 1. Check if there is a leaderboard value in localStorage (from previous games played).
let leaderboard = JSON.parse(localStorage.getItem('leaderboard'));

// 2. If none exists, create a new array
if (leaderboard === null) {
  leaderboard = [];
}

// 3. Add the score to the leaderboard
leaderboard.push(clickerBtn.dataset.clicks);

// 4. Store the updated leaderboard in localStorage
localStorage.setItem('leaderboard', JSON.stringify(leaderboard));

// 5. update the leaderboard display
updateLeaderboard(leaderboard);

Return to the game and test it out! It should work:

Pro Tip you will often see the following syntax when retrieving a value from localStorage

let leaderboard = localStorage.getItem('leaderboard') || [];

When used in this way, the || operator returns the left value (localStorage.getItem('leaderboard')) if it is truthy and returns the right value [] if the left value is falsy.

In this case, getItem() will return null (a falsy value) if there isn't a leaderboard stored in localStorage.

As a result, we don't need this code:

// 2. If none exists, create a new array
if (leaderboard === null) {
  leaderboard = [];
}

localStorage Helpers

Having to remember stringify our values before adding them to localStorage can be a pain. In addition, using JSON.parse can sometimes result in an error if the string we are parsing is not in valid JSON format.

For example, JSON objects need to have double-quotations ("") around the keys (single quotations won't work!):

JSON.parse(`{ "hello": "world" }`)
// This works fine

JSON.parse(`{ 'hello': 'world' }`);
//Uncaught SyntaxError: Expected property name or '}' in JSON at position 2 (line 1 column 3)

To ensure that we are always and consistently stringifying and parsing every time we interact with localStorage, we can write these two helper functions:

const setLocalStorageKey = (key, value) => {
  localStorage.setItem(key, JSON.stringify(value))
}

const getLocalStorageKey = (key) => {
  try {
    return JSON.parse(localStorage.getItem(key))
  } catch (err) {
    console.error(err);
    return null;
  }
}

In getLocalStorageKye, we use a try/catch block which is new syntax to us. It works by attempting to execute the code in the try {} block knowing that it could potentially throw an error. Normally, a thrown error would crash the program. But with the catch (err) {} block, we can "catch" the error and handle it without crashing the program.

  • If JSON.parse works, it will return the value.

  • If it throws an error, the error will be printed (and not break everything) and null will be returned.

Now, we can safely use these functions instead of the localStorage ones and know that all values will be properly stored and retrieved.

// so much cleaner!
let leaderboard = getLocalStorageKey('leaderboard');

if (leaderboard === null) {
  leaderboard = [];
}

leaderboard.push(clickerBtn.dataset.clicks);

// so much cleaner!
setLocalStorageKey('leaderboard', leaderboard);

updateLeaderboard(leaderboard);

The two helper functions that we've ensure that our interactions with localStorage won't throw any errors.

One way to think about these helper functions is that they form an API (application programming interface) for localStorage. In other words, these functions allow the rest of the program to access localStorage in a controlled manner.

We can take this even further to ensure that our application works in a consistent and predictable manner.

Data Layer: Creating an API for localStorage

Suppose we had an application with a form that lets users add names to a list. Names are displayed on the screen in a list and clicking on the name will remove it from the list. This application can be found in 3-data-layer.

Currently, it interacts with a names array stored in localStorage by directly using localStorage methods, manually stringifying and parsing. This opens the door to errors and inconsistency.

In order to restrict the program so that it can only interact with localStorage in the way we choose, we:

  • Isolate the logic for dealing with localStorage in its own file.

  • Create functions for interacting with the names key in localStorage.

  • Only export the functions that indirectly interact with localStorage. The exported functions will form our API.

/* 
These generic localStorage helpers will make the functions 
below easier to write. They handle directly interacting with
localStorage and dealing with stringifying / parsing values.

The remaining exported functions form our API for interacting 
with localStorage...
*/
const setLocalStorageKey = (key, value) => {
  localStorage.setItem(key, JSON.stringify(value))
}

const getLocalStorageKey = (key) => {
  try {
    return JSON.parse(localStorage.getItem(key))
  } catch (err) {
    console.error(err);
    return null;
  }  
}

/* 
This very basic helper only returns the 'names' from local storage. 
By exporting this function and NOT the getLocalStorageKey function,
we restrict how the rest of our program can access localStorage
*/
export const getNames = () => getLocalStorageKey('names');

/* 
It can be useful to initialize a key in localStorage if there isn't
any existing value.
*/ 
export const initializeNamesIfEmpty = () => {
  if (!getNames()) {
    setLocalStorageKey('names', ['ben', 'gonzalo', 'motun']);
  }
}

/* 
When inserting a new entry into localStorage, you will want to follow
this pattern: store what's currently in localStorage in an "in-memory"
variable, modify the in-memory value, update localStorage.
*/
export const addName = (name) => {
  const names = getNames();
  names.push(name);
  setLocalStorageKey('names', names);
}

/* 
Removing a value follows roughly the same pattern. That is, pull
from localStorage, modify, and update localStorage.
*/
export const removeName = (nameToRemove) => {
  const names = getNames();
  const filteredNames = names.filter((name) => name !== nameToRemove);
  setLocalStorageKey('names', filteredNames);
}

/* 
Replacing the value with an empty value
*/
export const removeAllNames = () => {
  setLocalStorageKey('names', []);
}

Note that not every function is exported.

  • setLocalStorageKey and getLocalStorageKey directly interact with localStorage and take care of stringifying, parsing, and error handling.

  • The remaining functions are all exported. They form our localStorage API ("application programming interface"), giving the rest of our application the ability to interact with localStorage, but only in the ways that we choose.

With this localStorage API that we've created, we can greatly simplify our code!

const handleSubmit = (e) => {
  e.preventDefault();
  const form = e.target;
  const nameValue = form.name.value;

  // adding to localStorage directly
  const storedNames = JSON.parse(localStorage.getItem('names'));
  storedNames.push(nameValue);
  localStorage.setItem('names', JSON.stringify(storedNames));

  renderNames();
  form.reset();
}
const handleSubmit = (e) => {
  e.preventDefault();
  const form = e.target;
  const newName = form.name.value

  // using the API to add to localStorage
  addName(newName);
  
  renderNames();
  form.reset();
}

Q: What makes this predictable and consistent?

Answer

This is predictable and consistent because

  • we control what the user of these functions can do (set, get, initialize, add, remove)

  • the caller of those exported functions doesn't directly interact with localStorage

  • the exported functions handle the interaction with localStorage

Sure, we can interact with localStorage outside of this file too but we should avoid that if we want to maintain the predictable and consistent behavior.

That file acts as a data layer. We might also decide to isolate our DOM manipulation code and create a DOM layer or create an event handling layer.

Using localStorage, we will build a data layer that is used to inform what is rendered, know as the view layer. When users interact with the view through the form, the data layer will be updated and we re-render the view.

This cycle of data > view > handle events > data looks like this:

Removing Values

The localStorage API provides two methods for removing values. You can either remove a single value or clear all values from localStorage:

  • localStorage.removeItem(key) — removes the value associated with the given key from localStorage.

  • localStorage.clear() — removes all key:value pairs from localStorage

localStorage.setItem('name', 'alice');
localStorage.setItem('email', 'alice@mail.com');
localStorage.setItem('age', '25');

console.log(localStorage.getItem('name')); // Prints 'alice'

localStorage.removeItem('name');

console.log(localStorage.getItem('name')); // Prints undefined

console.log(localStorage.getItem('name')); // Prints 'alice'
here
Key Terms
Lets Make a Leaderboard!
localStorage Lets Us Store Data That Persists
JSON.stringify() and JSON.parse()
Adding the leaderboard to LocalStorage
localStorage Helpers
Data Layer: Creating an API for localStorage
Removing Values
a game where the user is challenged to click a button as many times as they can in 5 seconds.