This is the 6th day of my participation in the August More Text Challenge
series
- Vue 3 basis
- Vue 3 dynamic effect
- Vue 3 components
- Modular API
- Vue Router Next
- Vuex 4
Vuex is a state management mode specially developed for vuue. Js applications. It extracts the shared state of all components in the application to the outside and adopts centralized storage management. And the corresponding rules ensure that the state changes in a predictable way.
At the heart of Vuex is the repository Store, which is basically a container containing all application-level shared state states and related methods to modify state.
💡 Each component can still own and manage its own private state.
The core concepts and how Vuex works are shown below. One principle must be followed in Vuex: the only way to change the state state in Vuex is to commit mutation.
💡 This article is about Vuex 4, or Vuex Next, which is an adaptation of Vue 3. The main content of this article is 🎉 to address the differences with Vuex 3.
💡 By defining and isolating concepts in state management, and by enforcing rules to maintain independence between views and states, Vuex makes code more structured and maintainable for large, single-page applications; However, these rules can make the code cumbersome and redundant. If you are developing a simple application, it is best not to use Vuex. A simple Store model is all you need.
Install the introduction
The latest version of Vuex can be imported using the CDN. This module exposes the Vuex object and creates a repository instance by calling its method createStore via 🎉
<script src="https://unpkg.com/vuex@4"></script>
Copy the code
💡 can also specify the version
<script src="https://unpkg.com/[email protected]/dist/vuex.global.js"></script>
Copy the code
It can also be installed through NPM
npm install vuex@next --save
Copy the code
Then import the Vuex and 🎉 create the route using the method createStore
Remember to install the Vuex plugin via app.use(Store), where App is the Vue application instance and Store is the repository instance, and “inject” the repository instance from the root component into all the child components. The repository instance can then be accessed from any component of the Vue through this.$store.
💡 if using the composite API in the option setup function, 🎉 can use the useStore() function to access the repository instance.
import { createApp } from 'vue'
import { createStore } from 'vuex'
const app = Vue.createApp({})
// Create a new store instance
// This contains the shared state of components and methods for modifying the state
const store = createStore({
state: {},
getters: {},
mutations: {},
actions: {}})Install the Store instance as a plug-in
app.use(store)
Copy the code
State
State is a single state tree shared by components, following the principle of SSOT, Single source of Truth, and unique data source, because a single state tree allows us to directly locate any specific state fragment and easily take a snapshot of the entire current application state during debugging.
💡 Single-state trees and modularity do not conflict
Defined in the Store option state, similar to the component’s data option.
const store = createStore({
/ / state
state: {
numbers: [0.1.2.3.4.5.6]}});Copy the code
The specific property state is then accessed in the component via this.$store.statename. Since Vuex’s state store is responsive, the state state read from store is generally used as component properties for calculation. In this way, whenever the corresponding state changes, the corresponding calculated properties are also recalculated and the associated DOM is triggered to update.
// The component Counter uses the store property count
const Counter = {
template: `<div>{{ count }}</div>`.computed: {
count () {
return this.$store.state.count
}
}
}
Copy the code
Vuex provides an auxiliary function, mapState, which can pass objects or arrays as arguments to make it easier to generate computed properties for components:
/ / component
import { mapState } from 'vuex' // In the separately built version, the auxiliary function is vuex.mapstate
// ...
// Pass object for auxiliary function 'mapState' (key-value pairs can be used in various ways)
computed: mapState({
// Use the property name of state directly as the value. The key can be any name, thus renaming the property of state
countAlias: 'count'.// Use the arrow function, taking state as an input, to return one of the attributes of the required state
count: state= > state.count,
// Use a normal function that takes state as an input, so you can also get the local state of the current component (the data property) through this and return a new mixed state
countPlusLocalState(state) {
return state.count + this.localCount
}
})
Copy the code
/ / component
import { mapState } from 'vuex' // In the separately built version, the auxiliary function is vuex.mapstate
// ...
// Pass an array for the auxiliary function 'mapState' (the name of the computed property of the map is the same as the name of the property of state)
// Computed attributes only contain attributes derived from the state map
computed: mapState([
'count'
])
Calculated properties contain properties derived from the state map, as well as local (component-local) calculated properties.
// The mapState function returns an object. You can use the object expansion operator to deconstruct the objects returned by mapState and mix them with local computed properties in the component
computed: {
localComputed(){... },... mapState({... })}Copy the code
💡 Data state stored in Vuex follows the same rules as data in Vue instances, and the object must be pure plain
Getter
Getters derive states from state that define the store option getters, similar to what a component’s computed option does.
The first argument that a Getter takes is a state object, and (optionally) it takes getters as a second argument, which is an object containing other getters and returns a value or a function.
const store = createStore({
/ / state
state: {
numbers: [0.1.2.3.4.5.6]},// Getter
getters: {
oddNumbers(state) {
return state.numbers.filter((num) = > {
return num % 2})},evenNumbers(state) {
return state.numbers.filter((num) = > {
return (num % 2) - 1})},numbersLen: (state, getters) = > {
return getters.oddNumbers.length + getters.evenNumbers.length
}
}
});
Copy the code
Depending on the form of Getter, there are two ways to use getters in components:
-
The concrete Getter can be accessed as a property through this.$store.getters.getterName
-
You can also access the concrete Getter as a method call through this.$store.getters.getterName(params) (correspondingly, when defining the Getter, the return value should be a function)
// getter returns a function getters: { // ... getTodoById: (state) = > (id) = > { return state.todos.find(todo= > todo.id === id) } } Copy the code
// called in the component this.$store.getters.getTodoById(2) Copy the code
💡 If a Getter is accessed as a property, it can be considered a computed object of the store. It has the function of a dependency cache, that is, it is recalculated only when its dependency value changes. When a Getter is called in the form of a function, it can be regarded as store methods and will be called every time without caching the result.
Vuex provides a helper function, mapGetters, that makes it easier to mapGetters in a store to a component’s calculated properties by passing objects or arrays of strings as arguments:
import { mapGetters } from 'vuex'
// ...
// Pass an array for the auxiliary function 'mapGetters' (the name of the computed property of the map is the same as the name of the property of getters)
computed: {
// Mix getters into a computed object using the object expansion operator. mapGetters(['doneTodosCount'.'anotherGetter',])}Copy the code
import { mapGetters } from 'vuex'
// ...
// If you want to rename the getter property, pass the object to the auxiliary function 'mapGetters'
computed: {
...mapGetters({
/ / the ` enclosing doneCount ` mapping for ` enclosing $store. Getters. DoneTodosCount `
doneCount: 'doneTodosCount'})}Copy the code
Mutation
In order to be able to track state changes, it is important to follow the principle that the only way to change state state in Vuex is by committing mutation.
In the Store option Mutations, a mutation type (similar to an event type) and its mutation handler (a callback function) are defined as a function, in which the state state is modified.
The callback function takes state as the first argument, and the (optional) second argument, called payload, receives the data that is passed in.
const store = new Vuex.Store({
/ / state
state: {
count: 1
},
mutations: {
INCREMENT(state) {
// Change the stateThe state count++},SET_COUNT(state, n) {
// Change the state
state.count = n
}
}
});
Copy the code
💡 recommends constant (uppercase) mutationTypes assigned to variables, and computational attribute names when defining mutation handlers, both to allow tools like Linter to work when entering variables and to avoid duplicate code
// mutation-types.js
// Place all mutation names in a separate file
// Make available mutationType at a glance, easy to manage and maintain
export const SOME_MUTATION = 'SOME_MUTATION'
Copy the code
// store.js
import { createStore } from 'vuex'
import { SOME_MUTATION } from './mutation-types' // Introduce a list of mutaiton names
const store = createStore({
state: {... },mutations: {
// Use ES2015 style computed attribute naming, using a constant as the function name
[SOME_MUTATION] (state) {
// mutate state}}})Copy the code
The operations of ⚠️ in the mutation handler must be synchronous so that DevTools will accurately capture a snapshot of each mutation state before and after it.
In the component, commit a specific Mutation event type, similar to the triggering event. This will execute the corresponding Mutation handler to modify the corresponding property of state, and (optionally) the second parameter payload passed in when committing muation. Is passed as data to the mutation handler
this.$store.commit('SET_COUNT', { count: 10 })
Copy the code
💡 can also commit Mutation using an object style, passing an object containing a type attribute that specifies muationType, and passing other attributes as payload to the handler
store.commit({
type: 'SET_COUNT'.count: 10
})
Copy the code
MapMutations, an auxiliary function provided by Vuex, makes it easier to map the mutation in store into the component’s methods, which makes it easier to submit mutation directly in the component by calling the corresponding method. The function can pass objects or string arrays as parameters
import { mapMutations } from 'vuex'
export default {
// ...
methods: {
// It is an array. mapMutations([New value (); // Map this.increment() to this.store.com MIT ('increment')
Use this.$store.com MIT ('increment', amount);
'increment',]),// Object form, can be renamed method. mapMutations({add: 'increment' // Map this.add() to this.store.com MIT ('increment')}}})Copy the code
Action
Actions are similar to mutation and are functions, except that they are generally used to commit mutations rather than directly change the state, and can contain asynchronous operations. Mutation can only have synchronization operations.
An action type (similar to an event type) and its Action handler (a callback function) are defined in the store option Actions as functions to perform asynchronous operations and commit mutation
The callback takes context as the first argument, which is an object with the same methods and properties as the store instance, so you can submit a mutation by calling context.mit, Or get the state and getters from context.state and context.getters. You can deconstruct the context for convenience. The callback function can also optionally accept a second parameter, called payload, to receive the data that is passed in.
const store = createStore({
// ...
actions: {
increment (context, payload) {
context.commit('INCREMENT', payload)
},
decrement ({ commit }, payload) {
commit('DECREMENT', payload)
}
}
});
Copy the code
💡 Asynchronous operations are common in actions, so Vuex returns a Promise by default when distributing an Action in a component, If the action Handler callback also returns a Promise return new Promiser((resolve, reject) => {}), then the component can listen for the Promise of the action callback. Perform subsequent operations:
const store = createStore({
// ...
actionA ({ commit }) {
// Define an action that returns a Promise
return new Promise((resolve, reject) = > {
setTimeout(() = > {
commit('someMutation')
resolve()
}, 1000)})}});Copy the code
// Used in components
store.dispatch('actionA').then(() = > {
// ...
})
Copy the code
Dispatch a specific Action event type in a component similar to dispatch an event. The corresponding Action handler is executed, passing in (optionally) a second parameter as payload (typically an object) to pass data to the Action Handler
this.$store.dispatch('actionType')
Copy the code
Distribution of Action events as objects is also supported
store.dispatch({
type: 'incrementAsync'.amount: 10
})
Copy the code
Vuex provides an auxiliary function, mapActions, that makes it easier to mapActions from a store to a component’s methods. This makes it easier to distribute actions directly from the component by calling the corresponding method, which can pass objects or arrays of strings as arguments
import { mapActions } from 'vuex'
export default {
// ...
methods: {
// It is an array. mapActions(['increment'.// Map 'this.increment()' to 'this.$store.dispatch('increment')'
// 'mapActions' also supports payloads
// Map 'this.incrementBy(amount)' to 'this.$store.dispatch('incrementBy', amount)'
'incrementBy'
]),
// Object form, can be renamed method. mapActions({add: 'increment' // Map 'this.add()' to 'this.$store.dispatch('increment')'}}})Copy the code
Modular API
Several examples of composite apis are available.
Accessing a repository instance
If you use the combinatorial API in the option setup function, all Vuex provides the 🎉 function useStore() to access the repository instance, since this cannot access the component instance to call this.$store. This is equivalent to accessing the repository through this.$store in an optional API.
import { useStore } from 'vuex'
export default {
setup () {
const store = useStore()
}
}
Copy the code
Access State and Getter
Because the warehouse’s data is reactive, all 🎉 needs to create computed to “wrap” references to the warehouse’s state and Getter to preserve responsiveness, which is equivalent to creating computed properties in the optional API
import { computed } from 'vue'
import { useStore } from 'vuex'
export default {
setup () {
const store = useStore()
return {
// Access state in a computed function
count: computed(() = > store.state.count),
// Access the getter in a computed function
double: computed(() = > store.getters.double)
}
}
}
Copy the code
Commit Mutation and distribute Action
To use mutation and action, you simply call the commit() and dispatch() functions of the repository instance in the setup hook function.
import { useStore } from 'vuex'
export default {
setup () {
const store = useStore()
return {
/ / using mutation
increment: () = > store.commit('increment'),
/ / use the action
asyncIncrement: () = > store.dispatch('asyncIncrement')}}}Copy the code
Module
Vuex allows us to divide the Store into modules, each of which has its own state, mutations, actions, getters, and also supports nested sub-modules.
Register the module in the Store option modules
const moduleA = {
state: () = >({... }),// Use the function form and return an object
mutations: {... },actions: {... },getters: {... }}const store = createStore({
modules: {
a: moduleA,
}
})
Copy the code
The component can then access the local state of the corresponding module A with the module name this.$store.state.a
The state state in the ⚠️ module should take the form of a function and return an object containing attributes of the local state. Because we may sometimes need to create multiple instances of a module, we can make them independent of each other by returning objects from functions.
Local state
In the module, the first argument received by Mutation and Getter is the local state object in the module; For Action local state is exposed through context.state.
const moduleA = {
// local module state
state: () = > ({
count: 0
}),
getters: {
doubleCount (state) {
// `state` is the local module state
return state.count * 2}},mutations: {
INCREMENT (state) {
// `state` is the local module state
state.count++
}
},
actions: {
incrementIfOddOnRootSum ({ state, commit, rootState }) {
// `state` is the local module state
if (state.count<0) {
commit('INCREMENT')}}}}Copy the code
Accessing global status
Global state (root node state) can also be accessed in the module Getter, Action:
-
For the Getter in the module, the state of the root node is exposed in the third argument
const moduleA = { // ... getters: { sumWithRootCount (state, getters, rootState) { return state.count + rootState.count } } } Copy the code
-
For actions in modules, the root node state is exposed through context.rootState
const moduleA = { // ... actions: { incrementIfOddOnRootSum ({ state, commit, rootState }) { if ((state.count + rootState.count) % 2= = =1) { commit('increment')}}}}Copy the code
💡 rootState rootState actually contains the local state of all loaded modules (so modules can “use” rootState as an intermediary to access the local state of each other registered module)
The namespace
The module state is local, but actions, mutations, and getters inside the module are registered in the global namespace so that multiple modules can respond to a COMMIT or dispatch at the same time.
If you want a module to be more encapsulated and reusable (its getters, actions, and mutations don’t conflict with global registrations), you can make it a namespaced module by adding the option namespaced: True to the module.
const store = createStore({
modules: {
The account / / modules
account: {
namespaced: true.// Enable the namespace
state: () = >({... }),// The state in the module is already local
getters: {
isAdmin () { ... } Getters ['account/isAdmin']
},
actions: {
login () { ... } // 如果在组件中分发 dispatch('account/login')
},
mutations: {
login () { ... } // If commit('account/login') in component
},
// Nested modules
modules: {
// Inherits the parent module's namespace
myPage: {
state: () = >({... }),getters: {
profile () { ... } // -> getters['account/profile']}},// Further nested namespaces
posts: {
namespaced: true.state: () = >({... }),getters: {
popular () { ... } // -> getters['account/posts/popular']}}}}}})Copy the code
When the namespace is enabled, the module’s getters, Actions, and mutations become local, and they automatically change their names based on the path the module is registered with. If you want to access the Getter, commit Mutation, and distribute Action for the module, you need to call the module based on its registration path:
getters['moduleName/stateName']
dispatch('moduleName/actionType')
commit('moduleName/mutationType')
💡 However, there is no need to add path “prefix” to access Getter, submit Mutation and distribute Action inside each module, which is also easier for module migration and reuse.
Accessing global content
If you want to access global content within a module with a namespace
- For the Getter of the module, global state
rootState
And global GettersrootGetters
Will be the third and fourth arguments - For module actions,
context
The object of thecontext.rootState
和context.rootGetters
Expose global state and global Getters - If you want to distribute or commit an Action or Mutation for the global space within a module, you need to change the
{ root: true }
Pass as the third argumentdispatch
或commit
modules: {
foo: {
namespaced: true.// The namespace is enabled
getters: {
// Global state can be accessed using the third parameter of the getter 'rootState'
// The fourth argument 'rootGetters' accesses global Getters
someGetter (state, getters, rootState, rootGetters) {
getters.someOtherLocalGetter // -> 'foo/someOtherGetter'
rootGetters.someOtherGlobalGetter // -> 'someOtherGetter'
},
someOtherGetter: state= >{... }},actions: {
// Deconstruct context, where rootState and rootGetters are global space states and Getters respectively
someAction ({ dispatch, commit, getters, rootGetters }) {
getters.someGetter // -> 'foo/someGetter'
rootGetters.someGetter // -> 'someGetter'
dispatch('someOtherAction') // -> 'foo/someOtherAction'
// The third parameter of distribution can accept the 'root' attribute to access the root dispatch
dispatch('someOtherAction'.null, { root: true }) // -> 'someOtherAction'
commit('someMutation') // -> 'foo/someMutation'
// The third parameter at commit time can accept the 'root' attribute to access the root commit
commit('someMutation'.null, { root: true }) // -> 'someMutation'}, someOtherAction (ctx, payload) { ... }}}}Copy the code
Register global Action and Mutation
If you want to register global actions or mutations in modules that have namespace-enabled, you can add the root: true option to them and define the callback function in the option handler
modules: {
foo: {
namespaced: true.actions: {
someAction: {
root: true,
handler (namespacedContext, payload) { ... } // -> 'someAction'}}}}Copy the code
Auxiliary function mapping
When mapping using helper functions such as mapState or mapActions in a component, you can pass the module’s path as the first parameter to modules that have namespaced, so that all bindings automatically use the module as a context to simplify the code
/ / component
computed: {
...mapState({
a: state= > state.some.nested.module.a,
b: state= > state.some.nested.module.b
})
},
methods: {
...mapActions([
'some/nested/module/foo'.// -> this['some/nested/module/foo']()
'some/nested/module/bar' // -> this['some/nested/module/bar']()])}Copy the code
Simplify the way
/ / component
computed: {
...mapState('some/nested/module', {
a: state= > state.a,
b: state= > state.b
})
},
methods: {
...mapActions('some/nested/module'['foo'.// -> this.foo()
'bar' // -> this.bar()])}Copy the code
💡 Vuex also provides a createNamespacedHelpers method that takes the module’s path and returns an object from which you can deconstruct the binding helper functions relative to the module. For example, see the documentation.
Dynamic add and delete module
After a store is created, modules can still be registered using the store.registerModule method. You can use store.unregisterModule(moduleName) to dynamically unload modules. Module dynamic registration enables other Vue plug-ins to use Vuex to manage state by attaching new modules to the Store.
⚠️ But you cannot use this method to unload static modules, that is, modules declared in the configuration object when the repository instance is created
const store = createStore({ / * option * / })
// Register module 'myModule'
store.registerModule('myModule', {
// ...
})
// Register nested modules' myModule '
// Pass paths as arrays
store.registerModule(['nested'.'myModule'] and {// ...
})
Copy the code
💡 can check if the module has been registered with the Store using the store.hasModule(moduleName) method. For nested modules, moduleName needs to be passed as an array, not as a path string
Preserve module state
When uninstalling a Module, you might want to preserve the old state so that the original local state can be used when re-registering, such as preserving state for an application rendered from a server. Local state can be archived in the registration module setting option preserveState, which means that when a module is registered, its actions, mutations, and getters are added to the store, but state is not. This assumes that the original state in the repository instance already contains the state of this Module and that you do not want to overwrite it
store.registerModule('a'.module, {
preserveState: true
});
Copy the code