State

Single state tree

Vuex uses a single state tree — yes, it contains all the application-level state in a single object. So far it has been a “unique data source (SSOT)”. This also means that each app will contain only one Store instance. A single state tree allows us to directly locate any particular state fragment and easily take a snapshot of the entire current application state during debugging.

Single-state trees and modularity are not in conflict — we will discuss how to distribute states and state-change events across submodules in a later section.

Data stored in Vuex follows the same rules as data in Vue instances, such as that the state object must be plain. Vue#data (opens new window).

Get the Vuex state in the Vue component

So how do we present state in Vue components? Since Vuex’s state store is reactive, the easiest way to read state from a Store instance is to return some state in a compute property:

// Create a Counter component const Counter = {template: '<div>{{count}}</div>', computed: { count () { return store.state.count } } }Copy the code

Every time store.state.count changes, the calculated property is refetched and an update to the associated DOM is triggered.

However, this pattern causes components to rely on global state singletons. In a modular build system, state needs to be imported frequently in each component that needs to use state, and state needs to be simulated when testing components.

Vuex, via the Store option, provides a mechanism to “inject” state from the root component into each child component (by calling vue.use (Vuex)) :

Const app = new Vue({el: '#app', // Provide the store object to the 'Store' option, which can inject instances of store into all child stores, components: { Counter }, template: ` <div class="app"> <counter></counter> </div> ` })Copy the code

By registering the Store option in the root instance, the store instance is injected into all the children of the root component, which can be accessed through this.$store. Let’s update the implementation of Counter:

mapStateAuxiliary function

When a component needs to fetch multiple states, it can be repetitive and redundant to declare all those states as computed properties. To solve this problem, we can use the mapState helper function to help us generate calculated properties that will save you from pressing the key:

MapState import {mapState} from 'Vuex' export default {//... Computed: mapState({// Arrow function makes code more concise count: State => state.count, // Pass string argument 'count' equal to 'state => state.count' countAlias: CountPlusLocalState (state) {return state.count + this.localcount}})}Copy the code

We can also pass mapState an array of strings when the name of the computed property of the map is the same as the name of the child node of State.

Computed: mapState([// map this.count to store.state.count 'count'])Copy the code

Object expansion operator

The mapState function returns an object. How do we mix it with local computed properties? Typically, we need to use a utility function to merge multiple objects into one so that we can pass the final object to the computed property. But since the object expansion operator opens new Window, we can make it much simpler:

computed: { localComputed () { /* ... */}, // Use the object expansion operator to blend this object into an external object... mapState({ // ... })}Copy the code

The component still retains local state

Using Vuex does not mean that you need to put all the states into Vuex. While putting all the state in Vuex makes state changes more explicit and easier to debug, it also makes the code tedious and unintuitive. If there are states that belong strictly to a single component, it is best to treat them as local states of the component. You should make trade-offs and decisions based on your application development needs.

Getter

Sometimes we need to derive some state from the state in the store, such as filtering and counting lists:

computed: {
  doneTodosCount () {
    return this.$store.state.todos.filter(todo => todo.done).length
  }
}
Copy the code

If more than one component needs this property, we can either copy the function or extract a shared function and import it in multiple places — neither approach is ideal.

Vuex allows us to define “getters” (you can think of them as computed properties of the store) in the store. Just like evaluating properties, the return value of a getter is cached based on its dependency and is recalculated only if its dependency value changes.

The Getter accepts state as its first argument:

const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    }
  }
})
Copy the code

Access by property

Getters are exposed as store.getters objects, and you can access these values as properties:

store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
Copy the code

Getters can also accept other getters as second arguments:

getters: {
  // ...
  doneTodosCount: (state, getters) => {
    return getters.doneTodos.length
  }
}
Copy the code
store.getters.doneTodosCount // -> 1
Copy the code

We can easily use it in any component:

computed: {
  doneTodosCount () {
    return this.$store.getters.doneTodosCount
  }
}
Copy the code

Note that getters are cached as part of Vue’s responsive system when accessed through properties.

Access by method

You can also pass parameters to a getter by asking the getter to return a function. It’s useful when you’re querying an array in a store.

getters: {
  // ...
  getTodoById: (state) => (id) => {
    return state.todos.find(todo => todo.id === id)
  }
}
Copy the code
store.getters.getTodoById(2) // -> { id: 2, text: '... ', done: false }Copy the code

Note that the getter is called every time it is accessed through a method, without caching the result.

mapGettersAuxiliary function

The mapGetters helper function simply maps the getters in the store to local computed properties:


}
Copy the code

If you want to give a getter property another name, use object form:

. MapGetters ({/ / the ` enclosing doneCount ` mapping for ` enclosing $store. The getters. DoneTodosCount ` doneCount: 'doneTodosCount})Copy the code

Mutation

The only way to change the state in Vuex’s store is to commit mutation. Mutations in Vuex are very similar to events: each mutation has a string event type (type) and a callback function (handler). This callback is where we actually make the state change, and it takes state as the first argument:

Const store = new Vuex. Store ({state: {count: 1}, mutations: {increment (state) {/ / the status state. Count++}}})Copy the code

You cannot call a mutation handler directly. This option is more like event registration: “This function is called when a mutation of type INCREMENT is triggered.” To wake up a mutation handler, you need to call the store.mit method with the corresponding type:

store.commit('increment')
Copy the code

Payload submission

You can pass an additional parameter, payload, to store.mit:

// ...
mutations: {
  increment (state, n) {
    state.count += n
  }
}
Copy the code
store.commit('increment', 10)
Copy the code

In most cases, the payload should be an object, which can contain multiple fields and the mutation recorded will be more readable:

// ...
mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}
Copy the code
store.commit('increment', {
  amount: 10
})
Copy the code

Object style submission

Another way to submit mutation is to use an object containing the type attribute directly:

store.commit({
  type: 'increment',
  amount: 10
})
Copy the code

When using an object-style commit, the entire object is passed as a payload to the mutation function, so the handler remains unchanged:

mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}
Copy the code

The Mutation complies with the Vue response rules

Since the state in Vuex’s Store is responsive, the Vue component that monitors the state updates automatically when we change the state. This also means that mutation in Vuex requires the same precautions as mutation in Vue:

  1. It is best to initialize all required properties in your store in advance.
  2. When you need to add new properties to an object, you should
  • useVue.set(obj, 'newProp', 123)Or,
  • Replace an old object with a new one. For example, using the object expansion operator, we can write:
state.obj = { ... state.obj, newProp: 123 }Copy the code

Replace Mutation event types with constants

Substituting constants for mutation event types is a common pattern in various Flux implementations. This allows tools like Linter to work, and keeping these constants in a separate file allows your code collaborators to see what mutations are included in the entire app:

// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
Copy the code
// store.js import Vuex from 'vuex' import { SOME_MUTATION } from './mutation-types' const store = new Vuex.Store({ state: { ... Mutations: {// we can use ES2015 style computing attribute naming to use a constant as the function name [SOME_MUTATION] (state) {// mutate state}}})Copy the code

Whether or not you use constants is up to you — this can be very helpful on large projects that require multiple people to work together. But if you don’t like it, you don’t have to.

Mutation must be a synchronization function

An important rule to remember is that mutation must be a synchronization function. Why is that? Please refer to the following example:

mutations: {
  someMutation (state) {
    api.callAsyncMethod(() => {
      state.count++
    })
  }
}
Copy the code

Now imagine that we are debugging an app and looking at the mutation log in devtool. Each mutation is recorded, and DevTools needs to capture a snapshot of the previous state and the next state. However, the callback in the asynchronous function in mutation in the example above made this impossible: Because the callback had not yet been called when mutation was triggered, DevTools did not know when the callback was actually called — essentially any state change made in the callback was untraceable.

Commit Mutation in the component

You can use this. codeStore.mit (‘ XXX ‘) to commit mutation in the component, or use the mapMutations helper function to map methods in the component to a store.mit call (requiring store injection at the root node).

import { mapMutations } from 'vuex' export default { // ... methods: { ... Apply mutations ([' increments ', // map 'this.increment()' to 'this.store.com MIT (' increments ')' 'incrementBy' // Map 'this.incrementBy(amount)' to 'this.incrementBy(amount)'),... Apply mutations ({add: 'increment' // map 'this.add()' to 'this.store.com MIT ('increment')'})}}Copy the code

Next step: Action

Mixing asynchronous calls in mutation can make your program difficult to debug. For example, when you call two mutations containing asynchronous callbacks to change the state, how do you know when to call back and which to call first? That’s why we have to distinguish between these two concepts. In Vuex, mutation are all synchronous transactions:

Store.com MIT ('increment') // Any state change caused by increment should be completed at this moment.Copy the code

To handle asynchronous operations, let’s look at actions.

Action

Action is similar to mutation, except that:

  • The Action commits mutation rather than a direct state change.
  • Actions can contain any asynchronous operation.

Let’s register a simple action:

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})
Copy the code

The Action function accepts a context object with the same methods and properties as the store instance, so you can submit a mutation by calling context.mit. Or get state and getters via context.state and context.getters. When we cover Modules later, you’ll see why the context object is not the Store instance itself.

In practice, we’ll often use ES2015’s parameter deconstruction to simplify code (especially if we need to call commit many times) :

actions: {
  increment ({ commit }) {
    commit('increment')
  }
}
Copy the code

Distribution of the Action

Action is triggered by the store.dispatch method:

store.dispatch('increment')
Copy the code

At first glance, it seems unnecessary. Wouldn’t it be easier to just distribute mutation? In fact, this is not the case. Remember that mutation must implement this limitation synchronously? Action is not bound! We can perform asynchronous operations within an action:

actions: {
  incrementAsync ({ commit }) {
    setTimeout(() => {
      commit('increment')
    }, 1000)
  }
}
Copy the code

Actions support the same payloads and objects for distribution:

// Distribute store.dispatch('incrementAsync', {amount: 10}) // distribute store.dispatch({type: 'incrementAsync', amount: 10})Copy the code

Let’s look at a more practical shopping cart example that involves calling the asynchronous API and distributing multiple mutations:

actions: {checkout ({commit, state}, products) {const savedCartItems = [...state.cart.added] Commit (types.checkout_request) // The shopping API accepts a successful callback and a failed callback shop.buyProducts(products, CHECKOUT_SUCCESS => commit(types.checkout_failure, savedCartItems)}}Copy the code

Note that we are doing a series of asynchronous operations and recording side effects (that is, state changes) from the action through mutation.

Distribute the Action in the component

You use this.$store.dispatch(‘ XXX ‘) to distribute actions in the component, or use the mapActions helper function to map the component’s methods to a store.dispatch call (which requires injecting store at the root node first) :

import { mapActions } from 'vuex' export default { // ... methods: { ... MapActions (['increment', // Map 'this.increment()' to 'this.$store.dispatch('increment')' // 'mapActions' also supports payloads: 'incrementBy' // map 'this.incrementBy(amount)' to 'this.$store.dispatch('incrementBy', amount)']),... MapActions ({add: 'increment' // map 'this.add()' to 'this.$store.dispatch('increment')'})}}Copy the code

Combination of the Action

Actions are usually asynchronous, so how do you know when an Action ends? More importantly, how can we combine multiple actions to handle more complex asynchronous processes?

First, you need to understand that store.dispatch can handle the Promise returned by the handler of the triggered action, and that store.dispatch still returns promises:

actions: {
  actionA ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('someMutation')
        resolve()
      }, 1000)
    })
  }
}
Copy the code

Now you can:

store.dispatch('actionA').then(() => {
  // ...
})
Copy the code

This is also possible in another action:

actions: {
  // ...
  actionB ({ dispatch, commit }) {
    return dispatch('actionA').then(() => {
      commit('someOtherMutation')
    })
  }
}
Copy the code

Finally, if we use async/await (open new window), we can compose actions as follows:

// Suppose getData() and getOtherData() return Promise actions: { async actionA ({ commit }) { commit('gotData', await getData()) }, async actionB ({ dispatch, Commit}) {await dispatch('actionA') // await actionA to complete commit('gotOtherData', await getOtherData())}}Copy the code

A single store.dispatch can trigger multiple action functions in different modules. In this case, the returned Promise will not be executed until all triggering functions have completed.

Module

Because of the use of a single state tree, all the states of an application are grouped into one large object. When the application becomes very complex, the Store object can become quite bloated.

To solve these problems, Vuex allows us to split the Store into modules. Each module has its own state, mutation, action, getter, and even nested submodules — split the same way from top to bottom:

const moduleA = { state: () => ({ ... }), mutations: { ... }, actions: { ... }, getters: { ... } } const moduleB = { state: () => ({ ... }), mutations: { ... }, actions: { ... } } const store = new Vuex.Store({ modules: { a: moduleA, b: ModuleB}}) Store.state. a // -> moduleA status Store.state. b // -> moduleB statusCopy the code

Local state of a module

For mutation and getters inside a module, the first argument received is the module’s local state object.

const moduleA = { state: () => ({ count: 0 }), mutations: {increment (state) {// where the 'state' object is the local state of the module state.count++}}, getters: { doubleCount (state) { return state.count * 2 } } }Copy the code

Similarly, for actions within the module, the local state is exposed through context.state and the rootState is context.rootState:

const moduleA = {
  // ...
  actions: {
    incrementIfOddOnRootSum ({ state, commit, rootState }) {
      if ((state.count + rootState.count) % 2 === 1) {
        commit('increment')
      }
    }
  }
}
Copy the code

For getters inside the module, the root node state is exposed as a third parameter:

const moduleA = {
  // ...
  getters: {
    sumWithRootCount (state, getters, rootState) {
      return state.count + rootState.count
    }
  }
}
Copy the code

The namespace

By default, actions, mutations, and getters inside a module are registered in the global namespace — enabling multiple modules to respond to the same mutation or action.

If you want your modules to be more wrapped and reusable, you can make them namespaced by adding namespaced: True. When a module is registered, all its getters, actions, and mutations are automatically named according to the path the module was registered with. Such as:

Const store = new vuex.store ({modules: {account: {namespaced: true, // Module assets) state: () => ({... }), // module states are already nested, and using the 'namespaced' attribute doesn't affect them. Getters: {isAdmin () {... } // -> getters['account/isAdmin'] }, actions: { login () { ... } // -> dispatch('account/login') }, mutations: { login () { ... } / / - > commit (' account/login ')}, / / nested modules modules: {myPage / / father module namespace: {state: (a) = > ({... }), getters: { profile () { ... } // -> getters['account/profile']}}, // Further nested namespace posts: {namespaced: true, state: () => ({... }), getters: { popular () { ... } // -> getters['account/posts/popular'] } } } } } })Copy the code

Namespace-enabled getters and actions receive localized getters, dispatch, and commit. In other words, you do not need to add a space name prefix to the same module when using module assets. Changing the Namespaced attribute does not require modifying the code in the module.

Accessing Global Assets within a module with a namespace

If you want to use the global state and getter, rootState and rootGetters are passed to the getter as the third and fourth arguments, and the action is passed to the context object’s properties.

To distribute action or commit mutation within the global namespace, pass {root: true} as the third argument to Dispatch or COMMIT.

modules: { foo: { namespaced: true, getters: {// In the getter of this module, 'getters' is localized // You can call' rootGetters' someGetter (state, getters, rootState, rootGetters) { getters.someOtherGetter // -> 'foo/someOtherGetter' rootGetters.someOtherGetter // -> 'someOtherGetter' }, someOtherGetter: state => { ... } }, actions: {// In this module, // They can accept the 'root' attribute to access the root dispatch or commit someAction ({dispatch, commit, getters, rootGetters }) { getters.someGetter // -> 'foo/someGetter' rootGetters.someGetter // -> 'someGetter' dispatch('someOtherAction') // -> 'foo/someOtherAction' dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction' commit('someMutation') // -> 'foo/someMutation' commit('someMutation', null, { root: true }) // -> 'someMutation' }, someOtherAction (ctx, payload) { ... }}}}Copy the code

Register global actions in namespaced modules

To register a global action in a namespaced module, add root: true and put the action definition in a function handler. Such as:

{
  actions: {
    someOtherAction ({dispatch}) {
      dispatch('someAction')
    }
  },
  modules: {
    foo: {
      namespaced: true,

      actions: {
        someAction: {
          root: true,
          handler (namespacedContext, payload) { ... } // -> 'someAction'
        }
      }
    }
  }
}
Copy the code

A binding function with a namespace

When using functions such as mapState, mapGetters, mapActions, and mapMutations to bind a module with a namespace, it can be cumbersome to write:

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

In this case, you can pass the module’s space name string as the first argument to the above function, so that all bindings automatically use the module as a context. Thus the above example can be simplified as:

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

In addition, you can create helper functions based on a namespace by using createNamespacedHelpers. It returns an object containing the new component binding helper function bound to the given namespace value:

import { createNamespacedHelpers } from 'vuex'

const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')

export default {
  computed: {
    // 在 `some/nested/module` 中查找
    ...mapState({
      a: state => state.a,
      b: state => state.b
    })
  },
  methods: {
    // 在 `some/nested/module` 中查找
    ...mapActions([
      'foo',
      'bar'
    ])
  }
}
Copy the code

Notes for plug-in developers

If you are developing a Plugin that provides modules and allows users to add them to the Vuex Store, you may need to consider the spatial name of the module. In this case, you can use the plugin’s parameter object to allow the user to specify the space name:

Export function createPlugin (options = {}) {return function (store) {// The space name added to the plugin module type (type) to const namespace = options. The namespace | | 'store. Dispatch (namespace +' pluginAction ')}}Copy the code

Module dynamic registration

After the store is created, you can register the module using the store.registerModule method:

Import Vuex from 'Vuex' const store = new Vuex.Store({/* options */}) // registerModule 'myModule' store.registerModule('myModule', {/ /... }) // Register nested modules 'nested /myModule 'store.registerModule(['nested', 'myModule'], {//... })Copy the code

Can be used after store. State. MyModule and store state. Nested. MyModule access module.

Module dynamic registration enables other Vue plug-ins to use Vuex to manage state by attaching new modules to the Store. For example, vuex-router-Sync (New Window) plug-in combines VUe-Router and VUex through dynamic registration module to implement application route status management.

You can also use store.unregisterModule(moduleName) to dynamically unload modules. Note that you cannot use this method to uninstall static modules (that is, modules declared when a store is created).

Note that you can check if the module has been registered with the store by using the store.hasModule(moduleName) method.

Keep the state

When registering a new Module, you will most likely want to preserve the state of the past, such as an application rendered from a server. You can archive this with the preserveState option: store.registerModule(‘a’, module, {preserveState: true}).

When you set preserveState: true, the module is registered and action, mutation, and getters are added to the store, but state is not. This assumes that the store state already contains the Module state and you don’t want to overwrite it.

Module reuse

Sometimes we may need to create multiple instances of a module, for example:

  • Create multiple stores that share the same module (e.g. whenrunInNewContextOptions arefalse 或 'once'In order toAvoiding stateful singletons in server-side rendering (Opens New Window))
  • Register the same module multiple times in a store

If we use a pure object to declare a module’s state, the state object will be shared by reference, leading to contamination of store or module data when the state object is modified.

This is actually the same problem with data in Vue components. So the solution is the same — use a function to declare the module state (supported only in 2.3.0+) :

Const MyReusableModule = {state: () => ({foo: 'bar'}), // mutation, Action and getter, etc... }Copy the code
From https://vuex.vuejs.org/zh/Copy the code