DEV Community

Thai Pangsakulyanont
Thai Pangsakulyanont

Posted on • Edited on

13 2

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.’ "

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Explore a sea of insights with this enlightening post, highly esteemed within the nurturing DEV Community. Coders of all stripes are invited to participate and contribute to our shared knowledge.

Expressing gratitude with a simple "thank you" can make a big impact. Leave your thanks in the comments!

On DEV, exchanging ideas smooths our way and strengthens our community bonds. Found this useful? A quick note of thanks to the author can mean a lot.

Okay