Recipe 2.4

Persisting Complex Objects

Fill out the user profile with a first and last name, then click Save. The profile data will be stored as an object in local storage. When you refresh the page, the previously saved profile data is shown.

The profile data includes a lastUpdated property that is automatically set when you click Save. Date objects can’t be serialized directly to JSON, but you can use a replacer and reviver function to serialize the Date to a compatible type, and convert it back to a Date object when deserializing it.

Demo

User Profile

Code

JavaScript
/**
 * Given a user profile object, serialize it to JSON and store it in local storage.
 * @param userProfile the profile object to save
 */
function saveProfile(userProfile) {
  localStorage.setItem('userProfile-complex', JSON.stringify(userProfile, replacer));
}

/**
 * Loads the user profile from local storage, and deserializes the JSON back to
 * an object. If there is no stored profile, an empty object is returned.
 * @returns the stored user profile, or an empty object
 */
function loadProfile() {
  return JSON.parse(localStorage.getItem('userProfile-complex'), reviver) || {};
}

function replacer(key, value) {
  if (key === '') {
    // first replacer call, `value` is the object itself.
    // Return all properties of the object, but transform `lastUpdated`.
    // This uses object spread syntax to make a copy of `value` before
    // adding the `lastUpdated` property.
    return {
      ...value, // make a copy, using object spread, to avoid altering the original object
      lastUpdated: value.lastUpdated.getTime()
    };
  }

  // After the initial transformation, the replacer is called once for each key-value pair.
  // No more replacements are necessary, so return these as is.
  return value;
}

function reviver(key, value) {
  // JSON.parse calls the reviver once for each key/value pair. Watch for the `lastUpdated` key.
  // Only proceed if there's actually a value for `lastUpdated`.
  if (key === 'lastUpdated' && value) {
    // Here, the value is the timestamp. You can pass this to the `Date` constructor
    // to create a `Date` object referring to the proper time.
    return new Date(value);
  }

  // Restore all other values as is.
  return value;
}

const form = document.querySelector('form');
const lastUpdated = document.querySelector('#last-updated');

const profile = loadProfile();
console.log('Loaded profile data:', profile);
form.elements.firstName.value = profile.firstName || '';
form.elements.lastName.value = profile.lastName || '';

if (profile.lastUpdated) {
  lastUpdated.textContent = `Last updated: ${profile.lastUpdated}`;
}

form.addEventListener('submit', event => {
  event.preventDefault();
  profile.firstName = form.elements.firstName.value;
  profile.lastName = form.elements.lastName.value;
  profile.lastUpdated = new Date();
  console.log('Updating profile lastUpdated date', profile.lastUpdated);
  console.log('Persisting profile:', profile);
  saveProfile(profile);

  lastUpdated.textContent = `Last updated: ${profile.lastUpdated}`;
});
HTML
<form>
  <div class="card">
    <div class="container-fluid card-body">
      <h2 class="card-title">User Profile</h2>
      <div class="row">
        <div class="col" id="last-updated"></div>
      </div>
      <div class="row">
        <div class="mb-3 col">
          <label for="firstName" class="form-label">First Name</label>
          <input autofocus type="text" name="firstName" id="firstName" class="form-control">
        </div>
        <div class="mb-3 col">
          <label for="lastName" class="form-label">Last Name</label>
          <input type="text" name=="lastName" id="lastName" class="form-control">
        </div>
      </div>
      <div class="row">
        <div class="col">
          <button class="btn btn-primary">Save</button>
        </div>
      </div>
    </div>
  </div>
</form>
Web API Cookbook
Joe Attardi