Vue is one of the most impactful and popular frontend frameworks from the last decade. Its ease of use won the hearts of many software enthusiasts ranging from beginners to experts alike.
But like many component based frameworks, data management becomes an issue as an app begins to scale. The need for shared state becomes obvious and discussions on the best solution are usually divisive and subjective.
Vue solves this with the use of a first party external package called Vuex. It's a state management library intended for use with Vue. It does this by abstracting the state and mutations (methods meant to change the state) into a store that's available for any component to use.
Let's create a simple Vuex store that has a list of items, methods to add and delete items, and a computed value to get the total number of items in the store.
// store.js
import Vue from 'vue';
import Vuex from 'vuex';
// register vuex as a plugin with vue in Vue 2
Vue.use(Vuex)
export default new Vuex.Store({
state: {
items: []
},
mutations: {
ADD_ITEM(state, item) {
state.items.push(item)
},
REMOVE_ITEM(state, id) {
state.items = state.items.filter(item => item.id !== id)
}
},
getters: {
totalLength: state => state.items.length
}
});
We'll create components that interact with the store.
// ItemForm.vue
<template>
<form @submit.prevent="handleSubmit">
<input v-model="value" required placeholder="Item Name">
</form>
</template>
<script>
export default {
data:() => ({value: ''}),
methods: {
handleSubmit(){
this.$store.commit('ADD_ITEM', {
id: Math.random().toString(),
name: this.value
});
this.value = ''
}
}
}
</script>
The ItemForm
component allows you to add new items to the store by committing the ADD_ITEM
mutation and passing the new item.
// Items.vue
<template>
<div>
<ul>
<li v-for="item in $store.state.items" :key="item.id">
<span>{{item.name}}</span>
<button @click="$store.commit('REMOVE_ITEM', item.id)">delete</button>
</li>
</ul>
<div>
Total Items: {{$store.getters.totalLength}}
</div>
</div>
</template>
The Items
component shows the list of items in the store and provides buttons for deleting each item by committing the REMOVE_ITEM
mutation. It also shows the total count of items in the store using the totalLength
getter.
// App.vue
<template>
<div>
<items />
<item-form/>
</div>
</template>
The App
component composes the Item
and ItemForm
components
Vue 3 brings a lot of new apis and features that make data and logic organization a lot cleaner and more reusable. Let's see how we can model the same behaviour using Composition API introduced in vue 3 as well as the existing provide/inject
api
// items-provider.js
import { reactive, computed, readonly } from "vue";
const state = reactive({
items: []
})
function addItem(todo) {
state.items.push(todo);
}
function removeItem(id) {
state.items = state.items.filter(item => item.id !== id);
}
const totalLength = computed(() => state.items.length);
export const itemStore = readonly({
state,
totalLength,
addItem,
removeItem
});
reactive
as the name implies, creates a reactive object that notifies its dependencies whenever its properties change. e.g, if the reactive object's property is referenced in a vue component's template, the component is registered as a dependency of that object's property and re-renders whenever that property changes.
computed
accepts a function and returns a memoized value that gets updated whenever any of the reactive values referenced in the callback function gets updated.
readonly
creates a read-only object. If an attempt is made to mutate any property on the object, a warning message is shown in the console and the operation fails.
Since the items-provider.js
file is a module, we only expose/export what we need (in this case, itemStore
). External modules and components shouldn't have direct access to mutate the items and properties of the store, so we expose a readonly version of the store.
We can now rewrite our components like so
// App.vue
<template>
<items />
<item-form />
</template>
<script>
import { itemStore } from './items-provider'
export default {
provide: {
itemStore
}
}
</script>
In the App
component we provide the itemStore
to make it injectable in any descendant component.
Also note that in vue 3, you aren't limited to just one root element per component
In the child components, we inject the itemStore
and it becomes available within the component's context.
// Items.vue
<template>
<ul>
<li v-for="item in itemStore.state.items" :key="item.id">
<span>{{item.name}}</span>
<button @click="itemStore.removeItem(item.id)">delete</button>
</li>
</ul>
<div>
Total Items: {{itemStore.totalLength}}
</div>
</template>
<script>
export default {
inject: ['itemStore']
}
</script>
// ItemForm.vue
<template>
<form @submit.prevent="handleSubmit">
<input v-model="value" required placeholder="Item Name">
</form>
</template>
<script>
export default {
inject: ['itemStore'],
data: () => ({value: ''}),
methods: {
handleSubmit(){
this.itemStore.addItem({
id: Math.random().toString(),
name: this.value
});
this.value = ''
}
}
}
</script>
Note that the
itemStore
is an export from items-provider module. It's not necessary to use theprovide/inject
api, you can simply import the itemStore into any component that needs the store, then expose it to the template viadata
,computed
ormethods
options on the component.
The major advantages to this approach are
- No extra overhead. We don't have to install an external data management tool. We just have to use tools that already exist within vue
- The store is read-only meaning it can only be modified my the explicitly defined functions, thereby enforcing one way data flow and eliminating unexpected behaviour.
- You can structure your store however you want. In the original example, the
itemStore
has a somewhat flat structure. The computed values and methods are directly on the store. We could just as easily create nested structure to group concerns like so
export const itemStore = readonly({
state: state,
getters: {
totalLength
},
actions: {
addItem,
removeItem
}
})
The downside to this approach is that this is not a dedicated data store solution, and because of that, the developer tooling for this approach is not as rich as Vuex (which has a dedicated section in vue devtools and a plethora of plugins).
Conclusion
This is only a base case scenario. For more complex scenarios involving SSR, it might make sense to use provider factories (functions that create or return a new store).
This is purely meant to demonstrate the power that vue provides out of the box and show you another way to store data in your vue application
And finally, this is my first write-up. It took me a while to gather the inertia to write this. Please leave a comment if you found this informative and let me know what you'd like me to write about next. Also i welcome constructive criticism, so don't hold back in the comments 😉.
Top comments (5)
Hi, thanks a lot for the great article. I write another one with a little bit more deeply explanation. I was inspired by you and I noticed it at the bottom of my text
kyle-ocean.dev/composition-api-ins...
Thank you. This is the first article I've found that explains Vue shared state with an actual example. It's very difficult to see what needs to be included in different components when looking at the Vue documentation. And the sample application created by vue-cli doesn't use any of these features.
Thank you very much. I have been searching around how to implement a simple state management in Vue3 without using Vuex. Your article has given me a great insight!
Might also be helpful tech.tham.xyz/vue3-state-managemen...
Written a very simple article recently on this topic
tech.tham.xyz/vue3-state-managemen...