Vuex
is a state management pattern designed specifically for Vue.js
applications. It adopts a centralized state management system for all components in the application, ensuring that the state changes in a predictable manner according to corresponding rules. The core of every Vuex
application is the store
, which is essentially a container that holds the majority of the application's state
.
In many business scenarios, different modular components indeed need to share data and perform modifications on it. This raises a contradiction in software design: the need for data sharing among modular components and the possibility of unpredictable results due to arbitrary data modifications. To resolve this contradiction, a design and architectural concept has been proposed in software design, which involves the unified management of global state and the requirement to follow a specific set of rules for operations such as accessing and modifying it. It's similar to the traffic rules that must be obeyed on the road, such as making a right turn on a zebra crossing. This concept provides a unified entry point for global state management, making the code structure clear and more maintainable. From the perspective of software design, the state management pattern is an architectural pattern that encapsulates global shared state data based on unified conventions and rules. You must conform to this design concept and architecture when performing CRUD
operations on shared state data in your project. Therefore, the so-called state management pattern is a kind of architectural pattern in software design.
The five core concepts of Vuex
can be summarized as follows:
state
: Basic data.getters
: Derived data from the basic data.mutations
: Methods for committing data changes, synchronous operations.actions
: Similar to a decorator, wrapping mutations
to make them asynchronous.modules
: Modularizing Vuex
.Vuex
uses a single state tree, which means that all the state data is contained in a single object. The state
is defined as a constructor option, which defines all the basic state parameters we need. In other words, the state
is the single source of truth (SSOT
), and each application will only contain one store
instance. The single state tree allows us to directly locate any specific state segment and easily obtain a snapshot of the entire current application state during debugging. Additionally, the single state tree does not conflict with modularization, as we can still distribute the state and state change events to different sub-modules. Using Vuex
does not mean that you need to put all the state into Vuex
. Although putting all the state into Vuex
makes state changes more explicit and easy to debug, it will also make the code lengthy and less intuitive. If some states strictly belong to a single component, it's best to keep them as the component's local state.
The simplest way to read the state from the store
instance is to return a specific state in a computed property. Since the state storage in Vuex
is reactive, whenever store.state.count
changes, the computed property will be recalculated, triggering a reactive update.
The mapState
function returns an object. When a component needs to access multiple states, declaring these states as computed properties may lead to redundancy. To address this issue, we can use the mapState
helper function to generate computed properties.
If there are also local computed properties in the current component that need to be defined, they can typically be mixed into an external object using the object spread operator ...
.
Getters
are the states derived from the state
of the store
. For example, we may need to filter and count the list, and if multiple components need a certain property, we would either need to copy the function for each or extract a shared function and import it in many places, neither of which is ideal. However, Vuex
allows us to define getters
in the store
(similar to computed properties in components). Just like computed properties, the return value of a getter
will be cached based on its dependencies and will only be recalculated when its dependencies change.
Getters
receive state
as their first argument and accept other getters
as the second argument. If not needed, the second argument can be omitted. Similar to state
, we can also access Vuex
's getters
through Vue
's Computed
.
mapGetters
Helper FunctionThe mapGetters
helper function maps the getters
in the store
to local computed properties, similar to state
.
Committing a mutation
is the only way to change the state in the Vuex
store, and mutation
must be synchronous; for asynchronous changes, actions
must be used.
Each mutation
has a string event type type
and a callback function handler
. The callback function is where we actually make the state changes, and it takes state
as the first argument and the payload as the second argument (the payload should mostly be an object and can also be omitted).
You cannot directly call a mutation handler
. This option is more like event registration. To invoke a mutation handler
, you need to call the store.commit
method with the corresponding type
.
Since the states in the Vuex
store are responsive, when we change the state, the monitoring Vue
components will automatically update. This also means that the mutation
in Vuex
needs to follow some considerations just like using Vue
:
store
in advance.Vue.set(obj, "newProp", 1)
, or replace the old object with a new one, for example state.obj = { ...state.obj, newProp: 1 }
.An important principle is that mutations must be synchronous functions. If we are debugging an app and observing the mutation log in devtools
, each mutation is recorded and devtools
needs to capture snapshots of the previous and next states. However, if asynchronous functions are used in the mutation, it makes this impossible because when the mutation is triggered, the callback function has not been called yet, and devtools
doesn't know when the callback function is actually called. In essence, any state changes made in the callback function are untraceable.
Mixing asynchronous calls in mutations will make your program difficult to debug. When you call two mutations containing asynchronous callbacks to change the state, you can't know when the callbacks will be called and which one will be called first. This is why the concepts of Mutation
and Action
need to be distinguished. In Vuex
, mutations are synchronous transactions, and any state changes caused by the committed keys should be completed at that moment.
Similar to other helper functions, you can use this.$store.commit("xxx")
to commit a mutation in the component, or use the mapMutations
helper function to map component methods to store.commit
calls.
Action
is similar to mutation
, the difference being that Action
commits a mutation
rather than directly changing the state, and Action
can contain any asynchronous operation.
The Action
function accepts a context
object with the same methods and properties as the store
instance, so you can call context.commit
to commit a mutation
, or use context.state
and context.getters
to access the state
and getters
.
Action
is triggered through the store.dispatch
method, and also supports dispatching with payloads and objects.
Using the mapActions
helper function can map the component's methods to store.dispatch
calls.
Actions
are usually asynchronous, and in some scenarios, we need to combine Actions
to handle more complex asynchronous processes. store.dispatch
can handle the Promise
returned by the triggered action
handling function, and store.dispatch
still returns a Promise
. One store.dispatch
can trigger multiple action
functions in different modules. In this case, the returned Promise
will only be executed after all the triggered functions are completed.
Due to the use of a single state tree, all application states are concentrated into a relatively large object. When the application becomes very complex, the store
object may become quite bulky. To solve the above issue, Vuex allows us to divide the store
into modules.
When performing module separation, each module has its own state
, mutation
, action
, getter
, and even nested sub-modules, i.e., the same way of division from top to bottom.
For the mutation
and getter
within the module, the first parameter received is the module's local state, and for the getter
within the module, the root state will be the third parameter.
Similarly, for the action
inside the module, the local state is exposed through context.state
, while the root state is context.rootState
.
By default, action
, mutation
, and getter
inside the module are registered in the global namespace, allowing multiple modules to respond to the same mutation
or action
. If you want your module to have higher encapsulation and reusability, you can make it a namespaced module by adding namespaced: true
. Once the module is registered, all its getter
, action
, and mutation
will automatically adjust their names based on the path of the module registration.
Enabled namespaced getter
and action
will receive localized getter
, dispatch
, and commit
. In other words, when using module assets, you do not need to add a namespace prefix within the same module, and you do not need to modify the code inside the module after changing the namespaced
attribute.
If you want to use global state
and getter
, rootState
and rootGetters
will be passed as the third and fourth arguments to the getter
, and also passed through the properties of the context
object to action
. If you need to dispatch an action
or commit a mutation
in the global namespace, pass { root: true }
as the third parameter to dispatch
or commit
.
If you need to register a global action
in a namespaced module, you can add root: true
and place the definition of this action
as a function handler
.
When using mapState
, mapGetters
, mapActions
, and mapMutations
functions to bind namespaced modules, it may be cumbersome to write. In such cases, you can pass the module's namespace string as the first argument to the above functions, so that all bindings automatically use the module as the context. Alternatively, you can create namespace-specific helper functions by using createNamespacedHelpers
. It returns an object containing component binding helper functions based on the given namespace value.
After creating the store
, you can use the store.registerModule
method to register modules. Then, you can access the module's state through store.state.myModule
and store.state.nested.myModule
. The module dynamic registration feature allows other Vue
plugins to use Vuex to manage state by attaching new modules to the store. For example, the vuex-router-sync
plugin combines vue-router
and vuex
by dynamically registering modules to manage the application's routing state. You can also use store.unregisterModule(moduleName)
to dynamically unload modules. Note that you cannot use this method to unload static modules, i.e., modules declared when creating the store. Additionally, you can use the store.hasModule(moduleName)
method to check if the module has already been registered with the store.