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 |
Top comments (1)
I appreciate the phase:
" I prefer ‘quick and a bit less dirty’ over ‘slow and perfect.’ "