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