Introduction
While building applications, one of the best practices is to make your application architecture component driven by using the “separation of concerns” concept. This also applies when building applications with Vue.
As you follow a component driven architecture, at some point in time, you’ll need to share data among these components.
How can we share data among these components in a Vue application?
Why Vuex ?
In a simple Vue application having just a few components, sharing data can be achieved using Props , Custom Event .
When your components start growing progressively, it’s advisable to introduce a Central Event Bus to serve a standalone service for managing data across the components in your application.
Eventually, your components will build up to form a tree where there will be parents, children, siblings, adjacent siblings etc.
For example, take a registration page which has three different stages. We may come up with four components — 3 to handle the stages and 1 to coordinate and manage the overall operations. You’ll see what I mean in a minute.
Managing data between the parent and children component (and other set of nested components) will get tricky and may easily be messed up while using the aforementioned ways of sharing data — Props and Custom Event
So, what’s the best way to share data among nested components?
The best way to handle data among these type of components is to introduce Vuex in your application.
Vuex can also be considered as a library implementation tailored specifically for Vue.js to take advantage of its granular reactivity system for efficient updates
Conceptually, Vuex may be pictured as a bucket of water that supplies water based on its content, and to whosoever needs it.
You cannot empty a bucket that’s not filled yet.
Vuex acts more or less like a central store for all components in the application — a bucket from which you can draw water. The store can be accessed by any of the components regardless of the number of (nested) components in an application.
Let's take a look at the architecture behind Vuex. If the architectural diagram seems a bit confusing, relax. You are definitely covered!
This article explains different modules of the architecture. We’ll use a familiar exampl: A counter system that either increments or decrements a counter state.
Getting Started
Vuex can easily be added to a project using any of the following options:
- CDN (Content Delivery Network)
Vuex is installed automatically immediately Vue is added
<script src="/path/to/vue.js"></script>
<script src="/path/to/vuex.js"></script>
2. NPM (Node Packet Manager)
npm install --save vuex
3. Yarn
yarn add vuex
Before we can access the properties of Vuex, Vue needs to be aware of the external resource, Vuex, before we can use it.
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
Vuex Fragments
For those who have some React.js background, Vuex
is a bit similar to a Redux or a Flux implementation. These are all based on the same general idea.
Based on the architectural diagram shown earlier, we’ll discuss the following modules:
1. State
Vuex majors on the idea of a store — where items belonging to the store can be shared easily. This central store holds the state of the application, and the state can either be modified, accessed or retrieved by any components in the application.
A state can also be assumed to be an observer that monitors the life cycle of a property. In this article, the property we’re monitoring is called counter.
Let's create a simple application which has two child components ( counter and display ) and a main component. The counter component has two buttons, increase to add 1 to the counter property, and decrease to reduce the counter by 1. The display component displays the current result of the counter while the main component combines both to make a single component.
The goal here is to either update (increase or decrease) the counter or get (display) the current value of the counter property. The state holds all the properties the application has. In this case, it has a counter property which is initially set to 0.
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
counter: 0
}
});
State pattern using a Central Store
How does the counter component gain access to Central Store?
Since we have made Vue aware of Vuex. We can now access Vuex properties without raising an alarm :)
<template>
<div>
<button class="btn btn-primary" @click="increase">Increase</button>
<button class="btn btn-danger" @click="decrease">Decrease</button>
</div>
</template>
<script>
export default {
name: "app-counter",
methods: {
increase() {
this.$store.state.counter++;
},
decrease() {
this.$store.state.counter--;
}
}
};
</script>
counter component
From the above code snippet, $store is a property from Vuex which gives access to the central store. This is how we access the state of the counter.
Two methods have also been defined_._ The increase method increases the current state of the counter by 1 while the decrease method decreases the current state of the counter by 1.
<template>
<p>
Counter value is: {{ counter }} </p>
</template>
<script>
export default {
name: 'appResult',
computed: {
counter() {
return this.$store.state.counter;
}
}
}
</script>
display component
In the display component shown above, the counter property is updated with the current counter state using computed property to display the result as the counter changes.
As simple as the state pattern above is, it can easily get messy when the current state needs to be modified and displayed across multiple components.
In the diagram above, the counter state is modified and displayed in Component R2, R3 and R4 respectively. Assuming the modification is the same, the same piece of code would be repeated in the three components. For example, adding a currency symbol to the counter state before being displayed in the components, the currency symbol would be repeated in all the three (3) components.
How can we stick to DRY (Do not Repeat Yourself) concept while accessing modified state(s) across components?
Another fragment we would look into is the getters, it works with the same concept of get in Javascript, and it returns the fed object.
2. Getters
Getters return the state in the central store. This ensures the state is not accessed directly from the store. It is also easier to modify the state before it’s accessed by any of the components in the application.
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
counter: 0
},
getters: {
/**
* access counter in state from the paramater
*/
addCurrencyToCounter: function (state) {
return `$ ${state.counter} (dollars)`;
}
}
});
Central Store with Getters
Let’s add a currency symbol to counter it being displayed in display component and see how getters work. addCurrencyToCounter (method in getters in snippet above) is accessed by the display component to get the current state of the counter.
To access the counter, addCurrencyToCounter is accessed in the object of $store called getters.
<template>
<p>
Counter value is: {{ counter }} </p>
</template>
<script>
export default {
name: 'appResult',
computed: {
counter() {
return this.$store.getters.addCurrencyToCounter;
}
}
}
</script>
display Component to display counter
What if there are lots of methods in the getter object, does the snippet becomes unnecessarily large?
Definitely yes! mapGetters is a helper object that maps all the getters functions to a property name.
mapGetters({
propertyName: 'methodName'
})
<template>
<div>
<p> Counter value is: {{ counter }} </p>
<p> Counter incremental value is: {{ increment }} </p>
</div>
</template>
<script>
import {
mapGetters
} from 'vuex';
export default {
name: 'appResult',
/**
* map the method(s) in getters to a property
*/
// computed: mapGetters({
// counter: 'addCurrencyToCounter',
// increment: 'incrementCounterByTen'
// })
/**
* **** using spread operator ****
* This is useful when other properties are to be
* added to computed proptery
*/
computed: {
...mapGetters({
counter: 'addCurrencyToCounter',
increment: 'incrementCounterByTen'
})
}
}
</script>
mapGetters
How do we know the components that modify the state?
Allowing the component(s) to modify the state directly without tracking which component modified the current state is not ideal. An example is an e-commerce application that has a checkout component, payment component etc. Imagine the itemPrice (state property) is modified by payment component without tracking which component had modified the state. This might result in unforeseen losses.
3. Mutation
Mutation uses the setter approach in getters and setters concept. Before we can access a property, it must have been set. The counter state was initially set to 0. In a situation where the counter needs to be set with a new value, mutation comes into play. It updates (commit) the states in the store.
The updated state done by mutation is now reflected in all components accessing the getters in the application.
Let's modify the above example by committing the changes from the counter component using mutation.
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
counter: 0
},
getters: {
/**
* access counter in state from the paramater
*/
addCurrencyToCounter: function (state) {
return `$ ${state.counter} (dollars)`;
},
incrementCounterByTen: function(state) {
return state.counter + 10
}
},
mutations: {
increase: function(state) {
state.counter ++;
},
decrement: function(state) {
state.counter++;
}
}
});
mutation
From the snippet above, the property of the state can be accessed from the parameter of the function. The state update can now be centralized in the central store. Even if the component is the 100th child of the parent, it can update the state and a child from a different parent can also have access to the state.
<template>
<div>
<button class="btn btn-primary" @click="increase">Increase</button>
<button class="btn btn-danger" @click="decrease">Decrease</button>
</div>
</template>
<script>
export default {
name: "app-counter",
methods: {
// increase() {
// this.$store.state.counter++;
// },
// decrease() {
// this.$store.state.counter--;
// }
increase() {
this.$store.commit('increase');
},
decrease() {
this.$store.commit('decrement');
}
}
};
</script>
Commit mutation methods
The commit property can also be accessed from $store to set the state to its current value. Apart from mapGetters used in mapping methods in getters to property names, there is also mapMutations which uses the same concept.
mapMutations({
propertyName: 'methodName'
})
Mutation would have been so perfect if it supported both synchronous and asynchronous operations. The methods we have observed so far are synchronous in operation.
Mutation has no chill. It’s only concerned about running a task immediately and making sure the state is accessible instantly.
As your web applications grow bigger, you’d likely want to connect to a remote server. This operation would definitely be treated as asynchronous operation since we can’t tell when the request would be done. If handled directly via mutations, the state would be updated beyond the expected result
How can we handle an asynchronous operation when dealing with mutations?
Since mutations would not run an async operation without messing with the state, its best to keep it out of it. We can always treat it outside mutation, and commit to state in mutation environs when the operation is done. This is where action comes in.
4. Action
Action is another fragment of Vuex. We can more or less call actions as a helper. It's a function that runs any sort of operation before letting mutation aware of what has been done. Its dispatched from the component and commits (updates) the state of mutation.
Now that the action handles the operation, the components have no business interacting with the mutations as we did earlier. The components only have to deal directly with the actions. The actions in central store can be accessed by the components using the object of $store called dispatch.
Let's take a quick look at how actions are placed in the central store.
Actions do not entirely erase the functions of mutations. As long as the operation we want to run is not async in nature, mutations can always take up the job.
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
counter: 0
},
getters: {
/**
* access counter in state from the paramater
*/
addCurrencyToCounter: function (state) {
return `$ ${state.counter} (dollars)`;
},
incrementCounterByTen: function(state) {
return state.counter + 10
}
},
mutations: {
increase: function(state) {
state.counter ++;
},
decrement: function(state) {
state.counter++;
}
},
actions: {
/**
* destruct the context, get the commit and call on the appropriate mutation
*/
increase: function({ commit }) {
commit('increase')
},
decrease: function({ commit }) {
commit('decrement');
},
/**
* demonstrate an async task
*/
asyncIncrement: function({ commit }) {
setTimeout(function(){
/**
* am done, kindly call appropriate mutation
*/
commit('increment')
}, 3000);
}
}
});
actions in central store
How does the counter component now have access to the actions ?
increase() {this.$store.dispatch('increase');}
The commit which belongs to mutations is simply replaced by dispatch belonging to actions.
Just like the way we have mapGetters and mapMutations, there is also mapActions which is mapped to all methods under actions in the central store.
...mapActions({
increase: 'increase',
decrease: 'decrease'
})
OR
...mapActions([
//this an es6 alternative for increment: 'increment'
'increase',
'decrease'
])
What we have been doing so far is a unidirectional data transfer. The central store has been distributing data to different components.
How do we now handle a bi-directional flow of data between central store and components ?
Getting data from the component, the data can easily be added alongside with the name of action.
this.$store.dispatch('actionName', data);
The second argument is the data (payload) that is sent to the store. It can be any type like string, number etc. I suggest the payload is always in form of an object to ensure consistency. This also provides the chance to pass in multiple data at the same time.
payload = {objValueA, objValueB, .... }
Considering an async operation asyncIncrement in the snippet below, which accepts a value from the component and delivers it to the mutation (commit) to update the state.
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
counter: 0
},
getters: {
/**
* access counter in state from the paramater
*/
addCurrencyToCounter: function (state) {
return `$ ${state.counter} (dollars)`;
},
incrementCounterByTen: function(state) {
return state.counter + 10;
}
},
mutations: {
increase: function(state) {
state.counter ++;
},
decrement: function(state) {
state.counter++;
},
asyncIncrement: function(state, incrementalObject) {
const { incrementalValue } = incrementalObject;
state.counter += incrementalValue;
}
},
actions: {
/**
* destruct the context, get the commit and call on the appropriate mutation
*/
increase: function({ commit }) {
commit('increase')
},
decrease: function({ commit }) {
commit('decrement');
},
/**
* demonstrate an async task
*/
asyncIncrement: function({ commit }, incrementalObject) {
setTimeout(function(){
/**
* am done, kindly call appropriate mutation
*/
commit('asyncIncrement', incrementalObject)
}, 3000);
}
}
});
central store
Let's add a new button to simulate the async process by adding 5 to the counter state when the operation is completed.
<template>
<div>
<button class="btn btn-primary" @click="increase">Increase</button>
<button class="btn btn-danger" @click="decrease">Decrease</button>
<button class="btn btn-info" @click="asyncIncrease(5)">Async Increase by 5</button>
</div>
</template>
<script>
import {
mapActions
} from 'vuex';
export default {
name: "app-counter",
methods: {
...mapActions({
increase: 'increase',
decrease: 'decrease'
}),
asyncIncrease(incrementalValue) {
const objectValue = {
incrementalValue
}
this.$store.dispatch('asyncIncrement', objectValue);
}
},
};
</script>
Conclusion
Vuex gives you the flexibility to manage multiple central stores based on the type of your project structure. You can also group your stores into modules. The modules act like a container to group more than one central store. This helps to manage stores properly belonging to different groups. Also, it's advisable to group method names which are created in mutations, actions and getters into a single object.
The source code to the project can be found here.
NB: The main components in most of the block diagrams were not connected so as to focus more on the point.
Plug: LogRocket, a DVR for web apps
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
Try it for free.
The post Managing multiple central stores with Vuex appeared first on LogRocket Blog.
Top comments (0)