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