The code and content in this article was updated toVue 3.0 on January 2021✨.
I’ve given a fewtalks on the multiple different ways one can manage application level state in a Vue.js application. Realizing a lot of the content I’ve shared might be useful if in a written format, I’ve prepared this article to summarize the majority of ways we can think about managing state (i.e. data) in a Vue application.
Vue components are the building blocks of Vue appsby allowing us to couple markup (HTML), logic (JS), and styles (CSS) within them.
Here’s an example of aSingle-File component that displays a series of numbers from a data property:
Each Vue component contains adata()
function that prepares the component to bereactive. If adata()
property value that’s being used in the template changes, the component view will re-render to show the change.
In the example above,numbers
is the array stored within thedata()
function. What ifnumbers
was a data value that needed to be accessed from another component? For example, we may need a component to be responsible in displayingnumbers
(like above) and another to manipulate the value ofnumbers
.
If we want to sharenumbers
between multiple components,numbers
doesn’t only become component level databut also application level data. This brings us to the topic ofState Management- the management of application level data.
Before we address how we can manage state in an application, we’ll begin by looking at howprops andcustom events can share data between parent and child components.
Assume we have a hypothetical application, that at first only contains a parent component and a child component. Like other front-end frameworks that exist, Vue gives us the ability to useprops to pass data from the parent down to the child.
Using props is fairly simple. All we essentially need to do is bind a value to the prop attribute where the child component is being rendered. Here’s an example of using props to pass an array of values down with the help of thev-bind directive:
TheParentComponent
passes thenumbers
array as props of the same name down toChildComponent
.ChildComponent
simply binds the value ofnumbers
on to its template with the help of theMustache syntax.
Here’s a CodeSandbox example of the above:
Note: All running code examples in this article are provided within a webpack bundled project based on thevue-cli
template. The code that we’ll be writing will only fit within thesrc/
folder of the projects.
#1:props can be used to pass data from parent components down to child components.
What if we needed to find a way to communicate information in the opposite direction? An example of this could be allowing the user to introduce a new number to the array presented in the example abovefrom the child component.
We can’t useprops
sinceprops
can only be used to pass data in a uni-directional format (from parent down to child down to grandchild…). To facilitate having the child component notify the parent about something, we can use Vuecustom events.
Custom events in Vue behave very similar tonative JavaScript custom eventsbut with one key distinction -Vue custom events are used primarily for communication between components as opposed to communication between DOM nodes.
Here’s an example of using custom events to have aChildComponent
be able to facilitate a change to aParentComponent
'snumbers
data property:
TheChildComponent
has an input that captures anumber
value and a button that emits anumber-added
custom event with the capturednumber
value.
On theParentComponent
, a custom event listener denoted by@number-added
, is specified where the child component is being rendered. When this event is emitted in the child, it pushes thenumber
value from the event toParentComponent
'snumbers
array.
Here’s a live example of this:
For some strange reason, the example app below doesn’t seem to work through CodeSandbox. If you’re interested in seeing the app function as intended below, open the sandbox, select the File tab and export as ZIP, and run the project in your local development environment.
#2:Custom events can be used to create communication from child to parent components.
We can use props to pass data downwards and custom events to send messages upwards. How would we be able to either pass data or facilitate communication between two different sibling components?
We can’t use custom events the way we have above because those events are emitted within the interface of a particular component, and as a result the custom event listener needs to be declared on where the component is being rendered. In two isolated components, one component isn’t being rendered within the other.
There are roughly 3 main ways we can begin to manage information between sibling components and as a result start to handle application wide state management:
An EventBus is an instancethat is used to enableisolated components to subscribe and publish custom events between one another.
Wait… didn’t we just say isolated components can’t trigger and listen to custom events between one another? They normally can’t, but an EventBus helps us achieve this by being madeglobal for this purpose.
As of Vue version 3.0, theVue documentation recommends us to use an external library to implement an event emitter interface within our app (i.e. implement an Event Bus). The external library we’ll use is themitt event emitter library.
Here’s an example of creating an instance of the Event Bus emitter within anevent-bus.js
file.
We can now use the interface of the EventBus to emit events. Let’s assume we have aNumberSubmit
component that’s responsible in sending a custom event when a button is clicked. This custom event,number-added
, will pass a value of what the user types in an input:
Now we can have a completely isolated component,NumberDisplay
, that’ll display a list of number values andlisten to if a new number is being entered inNumberSubmit
:
We’re attaching a listener,emitter.on
, on thecreated()
lifecycle hook of theNumberDisplay
component. WhenNumberSubmit
emits the event, it will pass anumber
value within the event object.NumberDisplay
listens and pushes that newnumber
to itsnumbers
data array.
Here’s a CodeSandbox of the example:
This answers the question we had in mind - An EventBus can be used to facilitate communication between sibling components:
Notice how easy it was to set up and use an EventBus? Unfortunately, an EventBus also brings along a clear disadvantage. Imagine our hypothetical application looked more like this:
Assume all the white lines are props that’s being passed from the parent down to all the children, and the yellow dashed lines are events being emitted and listened from and to a component. Each of these events aren’t being tracked and can be fired from anywhere in our application. This makes things hard to maintain really quickly which can make code frustrating to work with and can become a source of bugs.
This is the one of the main reasons as to why the Vue style-guide states that anEventBus is not the recommended approach to application wide data management.
#3: AnEventBus is an easy way to start having all components communicate with one another but doesn’t always scale well for medium to large applications.
Interested in learning Vue? WithFullstack Vue, you don’t have to read fifty different blog posts for hours on end. Instead, with this book, you’ll learnevery step required to build complete Vue apps and fit all the pieces together.
Let’s look to another way we can handle application data.
Simple state management can be performed by creating a store pattern that involves sharing a data store between components. The store can manage the state of our application as well as the methods that are responsible in changing the state.
For example, we can have a simple store like the following:
The store contains anumbers
array within its state, and anaddNumber
method that accepts a payload and directly updates thestate.numbers
value.
Notice the use of areactive
function to define the state object? With Vue 3.x, we’re able to import and use thereactive
function to declare reactive state from a JavaScript object. When this reactive state gets changed with theaddNumber
method, any component that uses this reactive state will automatically update!
We can have one component that’s responsible in displaying thenumbers
array from the store that we’ll callNumberDisplay
:
We can now have another component, calledNumberSubmit
, that will allow the user to add a new number to our data array:
TheNumberSubmit
component has anaddNumber()
method that calls thestore.addNumber()
mutation and passes the expected payload.
The store method receives the payload and directly mutates thestore.numbers
array. Thanks to Vue’s reactivity, whenever thenumbers
array in store state gets changed, the relevant DOM that depends on this value (<template>
ofNumberDisplay
)automatically updates.
When we say components interact with one another here, we’re using the term ‘interact’ loosely.The components aren’t going to do anything to each other but instead invoke changes to one anotherthroughthe store.
Here’s a CodeSandbox of the example we’ve laid out above:
If we take a closer look at all the pieces that directly interact with the store, we can establish a pattern:
NumberSubmit
has the responsibility to directlyact on the store method, so we can label it as astore action.NumberDisplay
doesn’t really care about what type of methods exist in the store or inNumberSubmit
, and is only concerned withgetting information from the store. So we’ll say Component A is astore getter of sorts.Anactioncommits to amutation. Themutation mutates state which then affects the view/components. View/components retrieve store data withgetters. We’re starting to getvery close to the Flux-like architecture of managing state.
#4: A simple store can be a more manageable way to manage application state by allowing components to depend on an external store.
Vuex is a Flux-like, state management library built solely for use with Vue.
For those who are unfamiliar -Flux is a design pattern created by Facebook. The Flux pattern is composed of 4 parts organized as a one-way data pipeline:
Vuex was inspired largely by Flux but also takes inspiration from theElm Architecture. The heart of a Vuex integration is the Vuex Store:
The Vuex Store is made complete with 4 objects - state, mutations, actions, and getters.
State is simply an object that contains the properties that need to be shared within the application:
This state object only has anumbers
array.
Mutations are functions responsible in directly mutating store state. In Vuex, mutations always have access to state as the first argument. In addition, Actions may or may not pass in a payload as the second argument:
In Flux architectures, mutation functions are often characterized in capital letters to distinguish them from other functions and for tooling/linting purposes. Above we have anADD_NUMBER
mutation that expects a payload and pushes that payload to thestate.numbers
array.
Actions exist to call mutations. Actions are also responsible in performing any or all asynchronous calls prior to committing to mutations. Actions have access to acontext
object that provides access to state (withcontext.state
), to getters (withcontext.getters
), and to the commit function (withcontext.commit
).
Here’s an example of an action that simply directly commits to a mutation while passing in the expected payload:
Getters are to a Vuex store whatcomputed properties are to a Vue component. Getters are primarily used to perform some calculation/manipulation to store state before having that information accessible to components.
Like mutations, getters have access to state as the first argument. Here’s a getter calledgetNumbers
that simply returns thestate.numbers
array:
For such a simple implementation like this, a Vuex store may not really be necessary. The examples above were provided to show the direct difference in implementation between using Vuex and a simple global store.
When a Vuex store is prepared, it’s made available to a Vue application by installing the store as a plugin in the Vue instance.
With a well built Vuex store, components often do one of two things. They either:
Here’s aNumberDisplay
component that directly displays thestate.numbers
array by mapping thegetNumbers
store getter on to the componentsgetNumbers
computed property.
ANumberSubmit
component can hold the responsibility to allow the user to add numbers tostate.numbers
by mapping anaddNumber
component method to the store action of the same name:
Here’s the above Vuex example in CodeSandbox:
We can see that Vuex extends the simple store method by introducingexplicitly defined actions, mutations, and getters. This is where the initial boilerplate as well as the main advantage comes in to using Vuex. In addition, Vuex integrates with the vue-devtools to providetime-travel debugging.
Here’s a quick gif on how the vue-devtools helps us observe store information as mutations occur, as well as being able to time-travel the UI to the moment a particular mutation has occurred.
Note: The app in the gif above is theTodoMVC example implementation.
Though Vuex does require us to establish some boilerplate, since Vuex is tailored to be used only for Vue applications - we can see how fairly easy it to integrate with on to a Vue application.
#5: Vuex extends a simple store method by introducing robust steps to having our application manage state.
Often times you may find people try to understand what the best approach is. I don’t necessarily believe there is acorrect orwrong approach since each of these methods come with their advantages and disadvantages.
-Pro: Incredibly easy to set-up.
-Con: Unable to properly track changes as they occur.
-Pro: Relatively easy to establish.
-Con: State and possible state changes aren’t explicitly defined.
-Pro: The most robust way to manage application level state and integrates with Vue dev-tools.
-Con: Additional boilerplate and requires Flux-like understanding.
At the end of the day, it’s up to us to understand what’s needed in our application and what the best approach may be.
I hope this was helpful and/or made you learn something new!
If you want to watch a slightly more detailed talk on this topic - here’s a video as well!
And finally, if you’ve never used Vue before and are interested in building Vue applications, feel free to check out Fullstack Vue. Since launch in 2018, Fullstack Vue has helped thousands of readers learn Vue. In early 2021, we’ve just updated the entire book to Vue version 3.0 🎉.
Hi! I’mHassan. I’m the author ofFullstack Vue, instructor/creator ofTinyHouse, and a Senior Front End Engineer atShopify.
If you have any questions, you can always connect with me at@djirdehh. If you enjoyed this article,give it a shoutout on Twitter!
Authors of Fullstack React, Fullstack Vue, and ng-book
Senior front-end engineer. Subscribe to my newsletter @https://largeapps.substack.com/