Now that we've seen some library options for implementing a Vue.js store (Vuex and Pinia), I want to spend a little time talking about some home rolled solutions. While home rolled solutions can be more fragile (if done hastily), less tested, and less standardized, they also provide an opportunity for useful customizations and the opportunity to learn more about stores and push the limits on what's possible.
First, let's talk about Vue.observable
for all of you still using Vue 2 and once we've seen the concepts employed here it will only be a short leap to using the Vue 3 Composition API as a store in the next article.
So where do we start rolling our own store in Vue 2? Vue 2.6 exposed a wonderful little method called observable
. Vue.observable()
made it possible to create reactive data outside of a Vue component, thus also making it possible to have a single source of truth (or state) that can be shared directly between multiple components.
Before we continue, if you haven't read the previous articles in this series, I encourage you to go back and at least look over articles 1. What is a Store and 2. Vuex, the Official Vue.js Store as we won't go much into principles in this article, but rather focus on implementation.
As opposed to Vuex or Pinia, Vue.observable
comes shipped with core Vue ^2.6.0, therefore there is no external package to install and setup. Easy enough! That means we can jump straight into defining our store!
Defining a store with Vue.observable()
can be approached in a number of different ways. Since it's a home rolled solution it's really up to you to decide exactly how you want to structure it, but the important piece is that your state is passed to Vue.observable
to make it reactive and that your store (or multiple stores) are defined in their own files to make them easily accessible in various components via import. When exporting the store from its dedicated file, I like to use a function that returns the state, that way it can easily be used as a computed prop once imported in the component.
// store/loggedInUser.js
import Vue from 'vue'
const state = Vue.observable({})
export default ()=> state
And if you'd like the extra protection of making sure that the state is never modified directly and only via your actions you can clone the state before returning it from the module.
// note both of these cloning methods have their limitations,
// for a more comprehensive clone you can use a lib like lodash
// shallow clone
export default ()=> ({...state})
// or deep clone
export default ()=> JSON.parse(JSON.stringify(state))
While this code may look a little low level, you could easily move it into a helper function that could be called on the default export of each newly created store.
Finally, accessing the store in the component is now as simple as importing it from the store and setting it as a computed prop.
//AppComponent.vue
<script>
import loggedInUser from "@/store/loggedInUser";
export default{
computed:{
loggedInUser
}
}
</script>
Now we've got our store defined but it's a little sad because it's not keeping up with any state. A store is nothing without it's state, so let's add some. We can do this by adding properties to the object passed to Vue.observable. And just like that our state is reactive! That's how powerful Vue.observable
is!
// store/loggedInUser.js
import Vue from 'vue'
const state = Vue.observable({
name: 'John Doe',
email: '[email protected]',
username: 'jd123'
})
Now we can access the state in the template.
// AppComponent.vue
<template>
<h1>Hello, my name is {{loggedInUser.name}}</h1>
</template>
<script>
import loggedInUser from "@/store/loggedInUser";
export default{
computed:{
loggedInUser
}
}
</script>
This approach is pretty straightforward but if you're fond of the mapState helper from Vuex you can recreate that with a few lines of code.
// helpers/store.js
// note: this only supports accessing the desired prop via an array of strings
export const mapState = (store, properties = [])=>{
const computed = {}
properties.forEach(prop => computed[prop] = ()=>store()[prop] )
return computed
}
// AppComponent.vue
<template>
<h1>Hello, my name is {{name}}</h1>
</template>
<script>
import loggedInUser from "@/store/loggedInUser";
import {mapState} from '@/helpers/store'
export default{
computed:{
...mapState(loggedInUser, ['name']),
}
}
</script>
Not only can Vue.observable
handle our reactive state but we can also combine it with a component's computed option to easily create getters. Under the hood a getter in Vuex or Pinia is nothing but a function, whose result can be cached based on it's dependencies, just like a computed prop on a component. So let's create a function that depends on a piece of state.
// store/loggedInUser.js
import Vue from 'vue'
const state = Vue.observable({
name: 'John Doe',
email: '[email protected]',
username: 'jd123',
posts: ['post 1', 'post 2', 'post 3', 'post 4']
})
export const postsCount = () => state.posts.length
Next, we can import it just as we did the state and set it as a computed prop. Setting it as a computed prop allows us not only to access it from the template, and without using the parentheses, but it also automatically caches the result of the function based on the values it depends on (it's dependencies) from the state defined with Vue.observable
.
// AppComponent.vue
<template>
<h1>Hello, my name is {{ name }}</h1>
<p>I have {{ postsCount }} posts available.</p>
</template>
<script>
import {
default as loggedInUser,
postsCount
} from "@/store/loggedInUser";
import {mapState} from '@/helpers/store'
export default{
computed:{
...mapState(loggedInUser, ['name']),
postsCount
}
}
</script>
With Vue.observable
you can of course define actions. If you've used the cloning approach described earlier, this in fact, will be the only way you're able to alter your state whether on purpose or on accident. There are a number of directions you could take to define actions but the important part is just that you provide them, in order to expose a controlled interface for modifying the store's state. I'll expose them here as individually exported functions from the store file as it makes them easy to assign as methods and makes it clear where the action is coming from.
// store/loggedInUser.js
import Vue from 'vue'
const state = Vue.observable({
// ...
posts: ['post 1', 'post 2', 'post 3', 'post 4']
})
export const insertPost = (post)=> state.posts.push(post)
Then in your component, after importing the action, you can call it wherever you'd like. The best part is, since it's just normal Javascript, any IDE that supports intellisense/autocomplete will be able to work more intelligently with it.
// AppComponent.vue
<template>
<!-- ... -->
<input v-model="post" type="text" />
<button @click="createNewPost(post)">Save</button>
</template>
<script>
import {
// ...
insertPost
} from "@/store/loggedInUser";
export default{
//...
data(){
return { post: '' }
}
methods:{
createNewPost(post){
// do whatever here you'd like, maybe validate post and then...
insertPost(post)
}
}
}
</script>
If you only need to be able to call the action from the template you can assign it directly as a method via the object property shorthand.
// AppComponent.vue
<template>
<!-- ... -->
<input v-model="post" type="text" />
<button @click="insertPost(post)">Save</button>
</template>
<script>
import {
// ...
insertPost
} from "@/store/loggedInUser";
export default{
//...
data(){
return { post: '' }
}
methods:{ insertPost }
}
</script>
You may not feel the difference just reading the code above but when actually putting the Vue.observable
approach into practice, actions simply feel more natural as they are just plain Javascript functions. No need to dispatch or commit anything.
With the direction we've taken so far we can treat our store just as Pinia does: as modular by default. We can just create new domain specific files for each store or module and only import them as needed. Alternatively you could probably make a single entry point for your store and treat it more like Vuex does but I don't see any real benefit to doing so, therefore we will not explore that any further in this article.
As far as devtools is concerned, the Vue.observable()
approach really offers the least amount of support. There is absolutely no time traveling available and their is no dedicated panel for viewing your store's state. Despite that, however, you can still view the state on the computed prop of the component you've imported it into and since you're already importing it and assigning it to computed anyway this really just means you're looking at the state in a panel that's not dedicated to the store.
To wrap things up for Vue.observable
as a store solution, let's just take quick recap of its most notable features to help you in your decision for implementing the store best fit for you and your project.
Despite the list of notable features I think I'd be remiss if I didn't mention some of it's (potentially crippling) drawbacks.
While Vuex and Pinia provide more standardized solutions, Vue.observable
provides the simplicity and ability to tailor your store to your exact needs and tastes. I wouldn't suggest using it on a large scale project with multiple team members because of it's lack of standardization but for small single developer projects it can be a quick lightweight solution that is quick to implement and if done well, can scale and be a pleasure to work with. In the next lesson, we'll continue home rolling stores, only next time using the latest and greatest: the Vue 3 Composition API.
Our goal is to be the number one source of Vue.js knowledge for all skill levels. We offer the knowledge of our industry leaders through awesome video courses for a ridiculously low price.
More than 200.000 users have already joined us. You are welcome too!
© All rights reserved. Made with ❤️ by BitterBrains, Inc.