DEV Community

Thai Pangsakulyanont
Thai Pangsakulyanont

Posted on • Edited on

Simple localStorage binding for Vue 2.x

tl;dr:

const localStorageValue = (key, defaultValue) =>
  new Vue({
    data: {
      value: defaultValue,
    },
    created() {
      const value = localStorage.getItem(key)
      if (value != null) this.value = value
    },
    watch: {
      value(value) {
        localStorage.setItem(key, value)
      },
    },
  })

Note: This article is written for Vue 2. For Vue 3, you can use this in your setup function:

const useLocalStorageValue = (key, defaultValue) => {
  const value = Vue.ref(localStorage.getItem(key) ?? defaultValue)
  Vue.watch(value, (newValue) => {
    localStorage.setItem(key, newValue)
  })
  return value
}

Let's say I want to create a signboard app that let's user enter some text and display it on screen, in large type.

Since this app will be very simple, I don't think I will need to use any build tooling; for this project I find it unnecessary (this is my most favorite Vue feature).

This is all the HTML and JS I need.

<div id="app">
  <div class="settings" v-show="mode === 'settings'">
    <label>
      <span>Text: </span>
      <textarea v-model="text"></textarea>
    </label>
    <button @click="mode = 'display'">Show</button>
  </div>
  <div
    class="display"
    v-show="mode === 'display'"
    style="font-size: 200px;"
    @click="mode = 'settings'"
  >
    {{text}}
  </div>
</div>
<script src="https://unpkg.com/vue@2.6.11/dist/vue.min.js"></script>
<script>
  new Vue({
    el: "#app",
    data: {
      text: "Enter something",
      mode: "settings"
    }
  });
</script>

It works, but as soon as I refresh the page, everything I typed is lost.

The obvious next step is to put them in localStorage, and Vue’s docs has a guide for it! Anyhow, here’s the change:

       new Vue({
         el: "#app",
         data: {
-          text: "Enter something",
+          text: localStorage.signboardText || "Enter something",
           mode: "settings"
+        },
+        watch: {
+          text(value) {
+            localStorage.signboardText = value;
+          }
         }
       });

This looks simple enough, and it works.

Time to add more features. I want to change the colors (background and foreground) and the font (family and size).

I won’t cover the HTML changes (you can find it here) but here is the changed JavaScript:

       new Vue({
         el: "#app",
         data: {
           text: localStorage.signboardText || "Enter something",
+          fg: localStorage.signboardForegroundColor || "#ffffff", // <--+
+          bg: localStorage.signboardBackgroundColor || "#000000", //    |
+          fontFamily:                                             //    |
+            localStorage.signboardFontFamily ||                   //    |
+            "system-ui, Helvetica, sans-serif",                   //    |
+          fontSize: localStorage.signboardFontSize || "200px",    //    |
           mode: "settings"                                        //    |
         },                                                        //    |
         watch: {                                                  //    |
           text(value) {                                           //    |
             localStorage.signboardText = value;                   //    |
+          },                                                      //    |
+          fg(value) { // <----------------------------------------------+
+            localStorage.signboardForegroundColor = value; // <---------+
+          },
+          bg(value) {
+            localStorage.signboardBackgroundColor = value;
+          },
+          fontFamily(value) {
+            localStorage.signboardFontFamily = value;
+          },
+          fontSize(value) {
+            localStorage.signboardFontSize = value;
           }
         }
       });

As you can see, the more features I add, the more spread apart it becomes. There more lines of unrelated code there are between the data section and the corresponding watch section. The more I have to scroll. The more unpleasant it becomes to work with this codebase, and the more prone to error I am1.

To solve this problem, I created an “unmounted Vue instance factory function”2. This is the code shown at the top of this article.

const localStorageValue = (key, defaultValue) =>
  new Vue({
    data: {
      value: defaultValue,
    },
    created() {
      const value = localStorage.getItem(key)
      if (value != null) this.value = value
    },
    watch: {
      value(value) {
        localStorage.setItem(key, value)
      },
    },
  })

With that, my main Vue instance becomes much smaller:

       new Vue({
         el: "#app",
         data: {
-          text: localStorage.signboardText || "Enter something",
-          fg: localStorage.signboardForegroundColor || "#ffffff",
-          bg: localStorage.signboardBackgroundColor || "#000000",
-          fontFamily:
-            localStorage.signboardFontFamily ||
-            "system-ui, Helvetica, sans-serif",
-          fontSize: localStorage.signboardFontSize || "200px",
+          text: localStorageValue("signboardText", "Enter something"),
+          fg: localStorageValue("signboardForegroundColor", "#ffffff"),
+          bg: localStorageValue("signboardBackgroundColor", "#000000"),
+          fontFamily: localStorageValue(
+            "signboardFontFamily",
+            "system-ui, Helvetica, sans-serif"
+          ),
+          fontSize: localStorageValue("signboardFontSize", "200px"),
           mode: "settings"
-        },
-        watch: {
-          text(value) {
-            localStorage.signboardText = value;
-          },
-          fg(value) {
-            localStorage.signboardForegroundColor = value;
-          },
-          bg(value) {
-            localStorage.signboardBackgroundColor = value;
-          },
-          fontFamily(value) {
-            localStorage.signboardFontFamily = value;
-          },
-          fontSize(value) {
-            localStorage.signboardFontSize = value;
-          }
         }
       });

I also had to change my template to refer to the value inside.

       <div class="settings" v-show="mode === 'settings'">
         <label>
           <span>Text: </span>
-          <textarea v-model="text"></textarea>
+          <textarea v-model="text.value"></textarea>
         </label>
         <label>
           <span>Foreground: </span>
-          <input type="color" v-model="fg" />
+          <input type="color" v-model="fg.value" />
         </label>
         <label>
           <span>Background: </span>
-          <input type="color" v-model="bg" />
+          <input type="color" v-model="bg.value" />
         </label>
         <label>
           <span>Font: </span>
-          <input v-model="fontFamily" />
+          <input v-model="fontFamily.value" />
         </label>
         <label>
           <span>Font size: </span>
-          <input v-model="fontSize" />
+          <input v-model="fontSize.value" />
         </label>
         <button @click="mode = 'display'">Show</button>
       </div>
       <div
         class="display"
         v-show="mode === 'display'"
-        :style="{ background: bg, color: fg, fontFamily: fontFamily, fontSize: fontSize }"
+        :style="{ background: bg.value, color: fg.value, fontFamily: fontFamily.value, fontSize: fontSize.value }"
         @click="mode = 'settings'"
       >
-        {{text}}
+        {{text.value}}
       </div>

This has helped me keeping the code a bit more cohesive, and reduced the amount of duplicated code between data and watch section.

I wouldn't say this is a best practice, but it works well enough for me, helped me solve this problem really quickly, and made the code a bit more cohesive at the same time. Unlike Scoped Slots (another really good technique), this one doesn't require me to make a lot of changes to the template to get all the bindings wired up. I prefer ‘quick and a bit less dirty’ over ‘slow and perfect.’ Maybe that can come later… but I can say little acts of code cleaning do add up.

Footnotes
1

I like to quantify the pleasantness of working on a codebase by amount of scrolling and file-switching required to add, change or delete a functionality. I talked about this concept of “cohesion” in my 2016 talk Smells in React Apps but I think it applies equally to Vue.

2

I'm not sure what is the name for this technique where you create a Vue instance without mounting it to any element.

I have heard about the terms headless components and renderless components, but they seem to be talking about an entirely different technique: the one where you use scoped slots to delegate rendering in a way akin to React’s render props.

In contrast, the technique I'm showing here doesn't even create a component, just a Vue instance that doesn’t get mounted to any element.

There is a misconception, as quoted from a book about Vue, that “without [the el option], Vue.js cannot function; it’s required.” Vue works just fine without an element to mount on — it stays in an unmounted state, but the following still functions: data observation, computed properties, methods, and watch/event callbacks.

Top comments (1)

Collapse
 
korrio_97 profile image
kOrriO~👨🏾‍💻

I appreciate the phase:
" I prefer ‘quick and a bit less dirty’ over ‘slow and perfect.’ "