Simple localStorage binding for Vue 2.x
Tags: vue, softwareengineering
Published: 2020-04-12 16:04:58

---
title: Simple localStorage binding for Vue 2.x
published: yes
description: using unmounted Vue instance factory function
tags:
- Vue
- SoftwareEngineering
---

tl;dr:

```js
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:

```js
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.

```html
<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](https://24v3g.csb.app/index-v1.html), 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](https://vuejs.org/v2/cookbook/client-side-storage.html)! Anyhow, here’s the change:

```diff
       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](https://24v3g.csb.app/index-v2.html).

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](https://24v3g.csb.app/index-v3.html)) but here is the changed JavaScript:

```diff
       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 am<sup><a href="#fn1" name="fnr1">1</a></sup>.

To solve this problem, I created an “unmounted Vue instance factory function”<sup><a href="#fn2" name="fnr2">2</a></sup>. This is the code shown at the top of this article.

```js
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:

```diff
       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.

```diff
       <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](https://vuejs.org/v2/guide/components-slots.html#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.

<table width="90%">
<col width="64">
<col>
<thead>
<tr><th colspan="2">Footnotes</th></tr>
</thead>
<tbody>
<tr>
<td valign="top" align="right"><a href="#fnr1">1</a></td>
<td>
<p><a name="fn1"></a>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 <a href="https://dt.in.th/talks/smells-in-react-apps/"><i>Smells in React Apps</i></a> but I think it applies equally to Vue.</p>
</td>
</tr>
<tr>
<td valign="top" align="right"><a href="#fnr2">2</a></td>
<td>
<p><a name="fn2"></a>I'm not sure what is the name for this technique where you create a Vue instance without mounting it to any element.</p>
<p>I have heard about the terms <a href="https://scotch.io/courses/build-an-online-shop-with-vue/components-and-the-vue-instance"><em>headless components</em></a> and <a href="https://css-tricks.com/building-renderless-vue-components/"><em>renderless components</em></a>, but they seem to be talking about an entirely different technique: the one where you use <a href="https://vuejs.org/v2/guide/components-slots.html#Scoped-Slots">scoped slots</a> to delegate rendering in a way akin to React’s <a href="https://reactjs.org/docs/render-props.html">render props</a>.</p>
<p>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.</p>
<p>There is a misconception, as quoted from a book about Vue, that “without [the <code>el</code> 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.</p>
</td>
</tr>
</tbody>
</table>