Quick LocalStorage Usage in Vue

localStorage can be an incredibly useful tool in creating experiences for applications, extensions, documentation, and a variety of use cases. I’ve personally used it in each! In cases where you’re storing something small for the user that doesn’t need to be kept permanently, localStorage is our friend. Let’s pair localStorage with Vue, which I personally find to be a great, and easy-to-read developer experience.

Simplified example

I recently taught a Frontend Masters course where we built an application from start to finish with Nuxt. I was looking for a way that we might be able to break down the way we were building it into smaller sections and check them off as we go, as we had a lot to cover. localStorage was a gsolition, as everyone was really tracking their own progress personally, and I didn’t necessarily need to store all of that information in something like AWS or Azure.

Here’s the final thing we’re building, which is a simple todo list:

Storing the data

We start by establishing the data we need for all the elements we might want to check, as well as an empty array for anything that will be checked by the user.

export default {
  data() {
    return {
      checked: [],
      todos: [
        "Set up nuxt.config.js",
        "Create Pages",
        // ...
        ]
     }
   }
}

We’ll also output it to the page in the template tag:

  <div id="app">
    <fieldset>
      <legend>
        What we're building
      </legend>
      <div v-for="todo in todos" :key="todo">
        <input
          type="checkbox"
          name="todo"
          :id="todo"
          :value="todo"
          v-model="checked"
        />
       <label :for="todo">{{ todo }}</label>
     </div>
   </fieldset>
 </div>

Mounting and watching

Currently, we’re responding to the changes in the UI, but we’re not yet storing them anywhere. In order to store them, we need to tell localStorage, “hey, we’re interested in working with you.” Then we also need to hook into Vue’s reactivity to update those changes. Once the component is mounted, we’ll use the mounted hook to select checked items in the todo list then parse them into JSON so we can store the data in localStorage:

mounted() {
  this.checked = JSON.parse(localStorage.getItem("checked")) || []
}

Now, we’ll watch that checked property for changes, and if anything adjusts, we’ll update localStorage as well!

watch: {
  checked(newValue, oldValue) {
    localStorage.setItem("checked", JSON.stringify(newValue));
  }
}

That’s it!

That’s actually all we need for this example. This just shows one small possible use case, but you can imagine how we could use localStorage for so many performant and personal experiences on the web!


The post Quick LocalStorage Usage in Vue appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

A Primer on the Different Types of Browser Storage

In back-end development, storage is a common part of the job. Application data is stored in databases, files in object storage, transient data in caches… there are seemingly endless possibilities for storing any sort of data. But data storage isn’t limited only to the back end. The front end (the browser) is equipped with many options to store data as well. We can boost our application performance, save user preferences, keep the application state across multiple sessions, or even different computers, by utilizing this storage.

In this article, we will go through the different possibilities to store data in the browser. We will cover three use cases for each method to grasp the pros and cons. In the end, you will be able to decide what storage is the best fit for your use case. So let’s start!

The localStorage API

localStorage is one of the most popular storage options in the browser and the go-to for many developers. The data is stored across sessions, never shared with the server, and is available for all pages under the same protocol and domain. Storage is limited to ~5MB.

Surprisingly, the Google Chrome team doesn’t recommend using this option as it blocks the main thread and is not accessible to web workers and service workers. They launched an experiment, KV Storage, as a better version, but it was just a trial that doesn’t seem to have gone anywhere just yet.

The localStorage API is available as window.localStorage and can save only UTF-16 strings. We must make sure to convert data to strings before saving it into localStorage. The main three functions are:

  • setItem('key', 'value')
  • getItem('key')
  • removeItem('key')

They’re all synchronous, which makes it simple to work with, but they block the main thread.

It’s worth mentioning that localStorage has a twin called sessionStorage. The only difference is that data stored in sessionStorage will last only for the current session, but the API is the same.

Let’s see it in action. The first example demonstrates how to use localStorage for storing the user’s preferences. In our case, it’s a boolean property that turns on or off the dark theme of our site.

You can check the checkbox and refresh the page to see that the state is saved across sessions. Take a look at the save and load functions to see how I convert the value to string and how I parse it. It’s important to remember that we can store only strings.

This second example loads Pokémon names from the PokéAPI.

We send a GET request using fetch and list all the names in a ul element. Upon getting the response, we cache it in the localStorage so our next visit can be much faster or even work offline. We have to use JSON.stringify to convert the data to string and JSON.parse to read it from the cache.

In this last example, I demonstrate a use case where the user can browse through different Pokémon pages, and the current page is saved for the next visits.

The issue with localStorage, in this case, is that the state is saved locally. This behavior doesn’t allow us to share the desired page with our friends. Later, we will see how to overcome this issue.

We will use these three examples in the next storage options as well. I forked the Pens and just changed the relevant functions. The overall skeleton is the same for all methods.

The IndexedDB API

IndexedDB is a modern storage solution in the browser. It can store a significant amount of structured data — even files, and blobs. Like every database, IndexedDB indexes the data for running queries efficiently. It’s more complex to use IndexedDB. We have to create a database, tables, and use transactions.

Compared to localStorage , IndexedDB requires a lot more code. In the examples, I use the native API with a Promise wrapper, but I highly recommend using third-party libraries to help you out. My recommendation is localForage because it uses the same localStorage API but implements it in a progressive enhancement manner, meaning if your browser supports IndexedDB, it will use it; and if not, it will fall back to localStorage.

Let’s code, and head over to our user preferences example!

idb is the Promise wrapper that we use instead of working with a low-level events-based API. They’re almost identical, so don’t worry. The first thing to notice is that every access to the database is async, meaning we don’t block the main thread. Compared to localStorage, this is a major advantage.

We need to open a connection to our database so it will be available throughout the app for reading and writing. We give our database a name, my-db, a schema version, 1, and an update function to apply changes between versions. This is very similar to database migrations. Our database schema is simple: only one object store, preferences. An object store is the equivalent of an SQL table. To write or read from the database, we must use transactions. This is the tedious part of using IndexedDB. Have a look at the new save and load functions in the demo.

No doubt that IndexedDB has much more overhead and the learning curve is steeper compared to localStorage. For the key value cases, it might make more sense to use localStorage or a third-party library that will help us be more productive.

Application data, such as in our Pokémon example, is the forte of IndexedDB. You can store hundreds of megabytes and even more in this database. You can store all the Pokémon in IndexedDB and have them available offline and even indexed! This is definitely the one to choose for storing app data.

I skipped the implementation of the third example, as IndexedDB doesn’t introduce any difference in this case compared to localStorage. Even with IndexedDB, the user will still not share the selected page with others or bookmark it for future use. They’re both not the right fit for this use case.

Cookies

Using cookies is a unique storage option. It’s the only storage that is also shared with the server. Cookies are sent as part of every request. It can be when the user browses through pages in our app or when the user sends Ajax requests. This allows us to create a shared state between the client and the server, and also share state between multiple applications in different subdomains. This is not possible by other storage options that are described in this article. One caveat: cookies are sent with every request, which means that we have to keep our cookies small to maintain a decent request size.

The most common use for cookies is authentication, which is out of the scope of this article. Just like the localStorage, cookies can store only strings. The cookies are concatenated into one semicolon-separated string, and they are sent in the cookie header of the request. You can set many attributes for every cookie, such as expiration, allowed domains, allowed pages, and many more.

In the examples, I show how to manipulate the cookies through the client-side, but it’s also possible to change them in your server-side application.

Saving the user’s preferences in a cookie can be a good fit if the server can utilize it somehow. For example, in the theme use case, the server can deliver the relevant CSS file and reduce potential bundle size (in case we’re doing server-side-rendering). Another use case might be to share these preferences across multiple subdomain apps without a database.

Reading and writing cookies with JavaScript is not as straightforward as you might think. To save a new cookie, you need to set document.cookie — check out the save function in the example above. I set the dark_theme cookie and add it a max-age attribute to make sure it will not expire when the tab is closed. Also, I add the SameSite and Secure attributes. These are necessary because CodePen uses iframe to run the examples, but you will not need them in most cases. Reading a cookie requires parsing the cookie string.

A cookie string looks like this:

key1=value1;key2=value2;key3=value3

So, first, we have to split the string by semicolon. Now, we have an array of cookies in the form of key1=value1, so we need to find the right element in the array. In the end, we split by the equal sign and get the last element in the new array. A bit tedious, but once you implement the getCookie function (or copy it from my example :P) you can forget it.

Saving application data in a cookie can be a bad idea! It will drastically increase the request size and will reduce application performance. Also, the server cannot benefit from this information as it’s a stale version of the information it already has in its database. If you use cookies, make sure to keep them small.

The pagination example is also not a good fit for cookies, just like localStorage and IndexedDB. The current page is a temporary state that we would like to share with others, and any of these methods do not achieve it.

URL storage

URL is not a storage, per se, but it’s a great way to create a shareable state. In practice, it means adding query parameters to the current URL that can be used to recreate the current state. The best example would be search queries and filters. If we search the term flexbox on CSS-Tricks, the URL will be updated to https://css-tricks.com/?s=flexbox. See how easy it is to share a search query once we use the URL? Another advantage is that you can simply hit the refresh button to get newer results of your query or even bookmark it.

We can save only strings in the URL, and its maximum length is limited, so we don’t have so much space. We will have to keep our state small. No one likes long and intimidating URLs.

Again, CodePen uses iframe to run the examples, so you cannot see the URL actually changing. Worry not, because all the bits and pieces are there so you can use it wherever you want.

We can access the query string through window.location.search and, lucky us, it can be parsed using the URLSearchParams class. No need to apply any complex string parsing anymore. When we want to read the current value, we can use the get function. When we want to write, we can use set. It’s not enough to only set the value; we also need to update the URL. This can be done using history.pushState or history.replaceState, depending on the behavior we want to accomplish.

I wouldn’t recommend saving a user’s preferences in the URL as we will have to add this state to every URL the user visits, and we cannot guarantee it; for example, if the user clicks on a link from Google Search.

Just like cookies, we cannot save application data in the URL as we have minimal space. And even if we did manage to store it, the URL will be long and not inviting to click. Might look like a phishing attack of sorts.

Just like our pagination example, the temporary application state is the best fit for the URL query string. Again, you cannot see the URL changes, but the URL updates with the ?page=x query parameter every time you click on a page. When the web page loads, it looks for this query parameter and fetches the right page accordingly. Now we can share this URL with our friends so they can enjoy our favorite Pokémon.

Cache API

Cache API is a storage for the network level. It is used to cache network requests and their responses. The Cache API fits perfectly with service workers. A service worker can intercept every network request, and using the Cache API, it can easily cache both the requests. The service worker can also return an existing cache item as a network response instead of fetching it from the server. By doing so, you can reduce network load times and make your application work even when offline. Originally, it was created for service workers but in modern browsers the Cache API is available also in window, iframe, and worker contexts as-well. It’s a very powerful API that can improve drastically the application user experience.

Just like IndexedDB the Cache API storage is not limited and you can store hundreds of megabytes and even more if you need to. The API is asynchronous so it will not block your main thread. And it’s accessible through the global property caches.

To read more about the Cache API, the Google Chrome team has made a great tutorial.

Chris created an awesome Pen with a practical example of combining service workers and the Cache API.

Bonus: Browser extension

If you build a browser extension, you have another option to store your data. I discovered it while working on my extension, daily.dev. It’s available via chrome.storage or browser.storage, if you use Mozilla’s polyfill. Make sure to request a storage permission in your manifest to get access.

There are two types of storage options, local and sync. The local storage is self-explanatory; it means it isn’t shared and kept locally. The sync storage is synced as part of the Google account and anywhere you install the extension with the same account this storage will be synced. Pretty cool feature if you ask me. Both have the same API so it’s super easy to switch back-and-forth, if needed. It’s async storage so it doesn’t block the main thread like localStorage. Unfortunately, I cannot create a demo for this storage option as it requires a browser extension but it’s pretty simple to use, and almost like localStorage. For more information about the exact implementation, refer to Chrome docs.

Conclusion

The browser has many options we can utilize to store our data. Following the Chrome team’s advice, our go-to storage should be IndexedDB. It’s async storage with enough space to store anything we want. localStorage is not encouraged, but is easier to use than IndexedDB. Cookies are a great way to share the client state with the server but are mostly used for authentication.

If you want to create pages with a shareable state such as a search page, use the URL’s query string to store this information. Lastly, if you build an extension, make sure to read about chrome.storage.


The post A Primer on the Different Types of Browser Storage appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

How to Make localStorage Reactive in Vue

Reactivity is one of Vue’s greatest features. It is also one of the most mysterious if you don’t know what it’s doing behind the scenes. Like, why does it work with objects and arrays and not with other things, like localStorage?

Let’s answer that that question, and while we’re at it, make Vue reactivity work with localStorage.

If we were to run the following code, we would see that the counter being displayed as a static value and not change like we might expect it to because of the interval changing the value in localStorage.

new Vue({
  el: "#counter",
  data: () => ({
    counter: localStorage.getItem("counter")
  }),
  computed: {
    even() {
      return this.counter % 2 == 0;
    }
  },
  template: `<div>
    <div>Counter: {{ counter }}</div>
    <div>Counter is {{ even ? 'even' : 'odd' }}</div>
  </div>`
});
// some-other-file.js
setInterval(() => {
  const counter = localStorage.getItem("counter");
  localStorage.setItem("counter", +counter + 1);
}, 1000);

While the counter property inside the Vue instance is reactive, it won’t change just because we changed its origin in localStorage

There are multiple solutions for this, the nicest perhaps is using Vuex and keep the store value in sync with localStorage. But what if we need something simple like what we have in this example? We have to take a dive in how Vue’s reactivity system works.

Reactivity in Vue

When Vue initializes a component instance, it observes the data option. This means it walks through all of the properties in data and converts them to getters/setters using Object.defineProperty. By having a custom setter for each property, Vue knows when a property changes, and it can notify the dependents that need to react to the change. How does it know which dependents rely on a property? By tapping into the getters, it can register when a computed property, watcher function, or  render function accesses a data prop.

// core/instance/state.js
function initData () {
  // ...
  observe(data)
}
// core/observer/index.js
export function observe (value) {
  // ...
  new Observer(value)
  // ...
}

export class Observer {
  // ...
  constructor (value) {
    // ...
    this.walk(value)
  }
  
  walk (obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
} 


export function defineReactive (obj, key, ...) {
  const dep = new Dep()
  // ...
  Object.defineProperty(obj, key, {
    // ...
    get() {
      // ...
      dep.depend()
      // ...
    },
    set(newVal) {
      // ...
      dep.notify()
    }
  })
}

So, why isn’t localStorage reactive? Because it’s not an object with properties.

But wait. We can’t define getters and setters with arrays either, yet arrays in Vue are still reactive. That’s because arrays are a special case in Vue. In order to have reactive arrays, Vue overrides array methods behind the scenes and patches them together with Vue’s reactivity system.

Could we do something similar with localStorage?

Overriding localStorage functions

As a first try, we can fix our initial example by overriding localStorage methods to keep track which component instances requested a localStorage item.

// A map between localStorage item keys and a list of Vue instances that depend on it
const storeItemSubscribers = {};


const getItem = window.localStorage.getItem;
localStorage.getItem = (key, target) => {
  console.info("Getting", key);


  // Collect dependent Vue instance
  if (!storeItemSubscribers[key]) storeItemSubscribers[key] = [];
  if (target) storeItemSubscribers[key].push(target);


  // Call the original function 
  return getItem.call(localStorage, key);
};


const setItem = window.localStorage.setItem;
localStorage.setItem = (key, value) => {
  console.info("Setting", key, value);


  // Update the value in the dependent Vue instances
  if (storeItemSubscribers[key]) {
    storeItemSubscribers[key].forEach((dep) => {
      if (dep.hasOwnProperty(key)) dep[key] = value;
    });
  }


  // Call the original function
  setItem.call(localStorage, key, value);
};
new Vue({
  el: "#counter",
  data: function() {
    return {
      counter: localStorage.getItem("counter", this) // We need to pass 'this' for now
    }
  },
  computed: {
    even() {
      return this.counter % 2 == 0;
    }
  },
  template: `<div>
    <div>Counter: {{ counter }}</div>
    <div>Counter is {{ even ? 'even' : 'odd' }}</div>
  </div>`
});
setInterval(() => {
  const counter = localStorage.getItem("counter");
  localStorage.setItem("counter", +counter + 1);
}, 1000);

In this example, we redefine getItem and setItem in order to collect and and notify the components that depend on localStorage items. In the new getItem, we note which component requests which item, and in setItems, we reach out to all the components that requested the item and rewrite their data prop.

In order to make the code above work, we have to pass on a reference to the component instance to getItem and that changes its function signature. We also can’t use the arrow function anymore because we otherwise wouldn’t have the correct this value.

If we want to do better, we have to dig deeper. For example, how could we keep track of dependents without explicitly passing them on?

How Vue collects dependencies

For inspiration, we can go back to Vue’s reactivity system. We previously saw that a data property’s getter will subscribe the caller to the further changes of the property when the data property is accessed. But how does it know who made the call? When we get a data prop, its getter function doesn’t have any input regarding who the caller was. Getter functions have no inputs. How does it know who to register as a dependent? 

Each data property maintains a list of its dependents that need to react in a Dep class. If we dig deeper in this class, we can see that the dependent itself is already defined in a static target variable whenever it is registered. This target is set by a so-far-mysterious Watcher class. In fact, when a data property changes, these watchers will be actually notified, and they will initiate the re-rendering of the component or the recalculation of a computed property.

But, again, who they are?

When Vue makes the data option observable, it also creates watchers for each computed property function, as well as all the watch functions (which shouldn’t be confused with the Watcher class), and the render function of every component instance. Watchers are like companions for these functions. They mainly do two things:

  1. They evaluate the function when they are created. This triggers the collection of dependencies.
  2. They re-run their function when they are notified that a value they rely on has changed. This will ultimately recalculate a computed property or re-render a whole component.

There is an important step that happens before watchers call the function they are responsible for: they set themselves as target in a static variable in the Dep class. This makes sure the they are registered as dependent when a reactive data property is accessed.

Keeping track of who called localStorage

We can’t exactly do that because we don’t have access to the inner mechanics of Vue. However, we can use the idea from Vue that lets a watcher set the target in a static property before it calls the function it is responsible for. Could we set a reference to the component instance before localStorage gets called?

If we assume that localStorage gets called while setting the data option, then we can hook into beforeCreate and created. These two hooks get triggered before and after initializing the data option, so we can set, then clear, a target variable with a reference to the current component instance (which we have access to in lifecycle hooks). Then, in our custom getters, we can register this target as a dependent.

The final bit we have to do is make these lifecycle hooks part of all our components. We can do that with a global mixin for the whole project.

// A map between localStorage item keys and a list of Vue instances that depend on it
const storeItemSubscribers = {};

// The Vue instance that is currently being initialised
let target = undefined;

const getItem = window.localStorage.getItem;
localStorage.getItem = (key) => {
  console.info("Getting", key);

  // Collect dependent Vue instance
  if (!storeItemSubscribers[key]) storeItemSubscribers[key] = [];
  if (target) storeItemSubscribers[key].push(target);

  // Call the original function
  return getItem.call(localStorage, key);
};

const setItem = window.localStorage.setItem;
localStorage.setItem = (key, value) => {
  console.info("Setting", key, value);

  // Update the value in the dependent Vue instances
  if (storeItemSubscribers[key]) {
    storeItemSubscribers[key].forEach((dep) => {
      if (dep.hasOwnProperty(key)) dep[key] = value;
    });
  }
  
  // Call the original function
  setItem.call(localStorage, key, value);
};

Vue.mixin({
  beforeCreate() {
    console.log("beforeCreate", this._uid);
    target = this;
  },
  created() {
    console.log("created", this._uid);
    target = undefined;
  }
});

Now, when we run our initial example, we will get a counter that increases the number every second.

new Vue({
  el: "#counter",
  data: () => ({
    counter: localStorage.getItem("counter")
  }),
  computed: {
    even() {
      return this.counter % 2 == 0;
    }
  },
  template: `<div class="component">
    <div>Counter: {{ counter }}</div>
    <div>Counter is {{ even ? 'even' : 'odd' }}</div>
  </div>`
});
setInterval(() => {
  const counter = localStorage.getItem("counter");
  localStorage.setItem("counter", +counter + 1);
}, 1000);

The end of our thought experiment

While we solved our initial problem, keep in mind that this is mostly a thought experiment. It lacks several features, like handling removed items and unmounted component instances. It also comes with restrictions, like the property name of the component instance requires the same name as the item stored in localStorage. That said, the primary goal is to get a better idea of how Vue reactivity works behind the scenes and get the most out of it, so that’s what I hope you get out of all this.

The post How to Make localStorage Reactive in Vue appeared first on CSS-Tricks.

The Browser Can Remember Edited Content

You can make the text inside any HTML element editable by adding the contenteditable attribute.

<div contenteditable>
  Hey, I'm like a textarea kinda now!
</div>

I wouldn't say there are wheelbarrows full of use-cases for that, but it's neat. One possible use might be an in-progress design in which editing the content from the design itself is useful either for you, or for someone else (a client?) who needs to change the text.

So, great, contenteditable. Now someone can click into the text and edit it.

There is nothing permanent about those changes. Refresh the page, look in another browser or whatever. Those edits are gone.

Say you wanted to do a little better job and make the changes persistent. You aren't trying to build a CMS here, or save the data through an authenticated connection to a database or anything. You just wanna make the edits to the text are maintained if the page refreshes.

One way is to chuck the data from the text changes you make into localStorage.

  1. When text is edited (on blur of the element), save the data to localStorage using a namespace and the
    ID of the element as the key.
  2. When the page loads, look through localStorage and see if there are any keys that match elements on the page and, if so, replace the content.
const editables = document.querySelectorAll("[contenteditable]");

// save edits
editables.forEach(el => {
  el.addEventListener("blur", () => {
    localStorage.setItem("dataStorage-" + el.id, el.innerHTML);
  })
});

// once on load
for (var key in localStorage) {
  if (key.includes("dataStorage-")) {
    const id = key.replace("dataStorage-","");
    document.querySelector("#" + id).innerHTML = localStorage.getItem(key);
  }
}

See the Pen
localStorage + contenteditable
by Chris Coyier (@chriscoyier)
on CodePen.


This reminds me of a few other things...

  • document.designMode = "on" is like a shortcut for making every element on the page behave like it has contenteditable.
  • Mavo is a little bit like this concept only with authentication, true data storage options, and editing UI.
  • If performance is a concern, KV storage is supposed to be a speeder async version of localStorage.

The post The Browser Can Remember Edited Content appeared first on CSS-Tricks.

KV Storage

localStorage is...

  • Good! It's an incredibly easy API to use.
  • localStorage.setItem('name', 'Chris'); let name = localStorage.getItem('name');
  • Bad! Philip Walton explains why:

localStorage is a synchronous API that blocks the main thread, and any time you access it you potentially prevent your page from being interactive.

Chrome has an idea (here's the proposal) for reinventing it. Ultimately the API is even simpler:

import { storage } from 'std:kv-storage';
storage.set('name', 'Chris');
storage.get('name');

But! It's async, so I can use await before I do those things without blocking anything. This demo will work in Chrome Canary right now:

See the Pen
eXadrq
by Chris Coyier (@chriscoyier)
on CodePen.

What in all heck is up with this line?

import { storage } from 'std:kv-storage';

They are calling it a "built-in module." In other words, something you can import but it makes no network request because it's built into the browser. Pretty interesting approach.

Philip continues:

Not exposing built-in modules globally has a lot of advantages: they won't add any overhead to starting up a new JavaScript runtime context (e.g. a new tab, worker, or service worker), and they won't consume any memory or CPU unless they're actually imported. Furthermore, they don't run the risk of naming collisions with other variables defined in your code.

This is built on top of indexedDB, so if you're playing with it and need to clear the values or whatever, you do it there (DevTools > Application > Storage > IndexedDB). It'll be fascinating to see if this catches on and whether new JavaScript features are shipped as built-in modules. I have no sense of whether other browsers think this is a good idea or not.

Direct Link to ArticlePermalink

The post KV Storage appeared first on CSS-Tricks.