What is vuex

Vuex is a state management mode developed specifically for vue.js applications. It uses centralized storage to manage the state of all components of an application and rules to ensure that the state changes in a predictable way

What is “state management mode”?

First, let’s take a look at the following example

new Vue({
  // state
  data () {
    return {
      count: 0
    }
  },
  // view
  template: `
    <div>{{ count }}</div>
  `,
  // actions
  methods: {
    increment () {
      this.count++
    }
  }
})
Copy the code

This state self-management application consists of the following parts:

  • State, the data source that drives the application;
  • View, to declaratively map state to the view;
  • Actions, in response to changes in state caused by user input on the view.

Here is a simple illustration of the idea of “one-way data flow” :

vuex.vuejs.org/flow.png

However, the simplicity of one-way data flow can easily be broken when our application encounters multiple component shared state:

  • Multiple views depend on the same state.
  • Actions from different views need to change the same state.

Vuex is a state management library designed specifically for vue.js to leverage the fine-grained data response mechanism of vue.js for efficient status updates.

When should I use Vuex?

Vuex helps us manage shared state and comes with more concepts and frameworks. This requires a trade-off between short-term and long-term benefits. Using Vuex can be tedious and redundant if you don’t plan to develop large, single-page applications. That’s true — if your application is simple, you’d better not use Vuex. A simple Store pattern is all you need. But if you need to build a medium to large single-page application, and you’re probably thinking about how best to manage state outside of components, Vuex would be the natural choice

The simplest Store

Specific code implementation

const store = new Vuex.Store({
  state: {
    token: 'xxx12345'Mutations: {changeToken(mutations, mutations) {mutations = token}}, mutations: {changeToken(mutations, mutations) {mutations = token}})'changeToken'.'xxxx12345555')
Copy the code

Again, we committed mutation rather than changing store.state.count directly because we wanted to track state changes more explicitly. This simple convention makes your intentions clear, making it easier to interpret in-app state changes as you read the code. In addition, this gives us the opportunity to implement debugging tools that record every state change and save a snapshot of the state. With it, we can even implement a time-travel debugging experience.

Because the state in a store is reactive, calling the state in a store in a component is as simple as simply returning it in a evaluated property. Triggering changes is also just a mutation submission in the component’s methods.

Vuex core concepts

State

Vuex uses a single state tree — yes, it contains all application-level state in a single object. At this point it exists as 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 — in a later chapter we will discuss how to distribute states and state change events across submodules

** Get Vuex state in 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 calculated property:

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

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

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'// Give the store object to the "store" option, which injects the store instance into all the child store components, Components: {trees}, template: '<div class="app">
      <trees></trees>
    </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 trees:

const trees = {
  template: `<div>{{ tree }}</div>`,
  computed: {
    count () {
      return this.$store.state.tree
    }
  }
}
Copy the code

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

Getters are exposed as store.getters. You can access these values as properties:

store.getters.doneTodos // -> [{ id: 1, text: '... '.done: true}] getters can also accept other getters as second arguments: getters: {//...doneTodosCount: (state, getters) => {
    returnGetters. DoneTodos. Length}} store. Getters. DoneTodosCount / / - > 1, 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.

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)
  }
}
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.

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) {
      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

** Submit the Payload **

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

mutations: {
  increment (state, n) {
    state.count += n
  }
}
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
  }
}
store.commit('increment', {
  amount: 10
})
Copy the code

** Object style submission method **

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

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

It is best to initialize all required properties in your store in advance.

When you need to add new properties to an object, you should

Use vue.set (obj, ‘newProp’, 123), or

Replace an old object with a new one. For example, using the stage-3 object expansion operator we could write:

state.obj = { … State. Obj, newProp: 123} ** Replacing Mutation event types with constants ** Replacing Mutation event types with constants 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'
// 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.

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.

You can use this. Codestore.com MIT (‘ XXX ‘) to commit Mutation in a component, Or use the mapMutations helper function to map methods in a component to a store.mit call (requiring store injection at the root node).

import { mapMutations } from 'vuex'

exportdefault { // ... methods: { ... mapMutations(['increment', // Map 'this.increment()' to 'this.$store.commit('increment') // 'mapMutations' also supports payload:'incrementBy'// Map 'this.incrementBy(amount)' to 'this.$store.commit('incrementBy', amount)` ]), ... mapMutations({ add:'increment'// Map 'this.add()' to 'this.$store.commit('increment') `}}}Copy the code

Action

The Action is similar to the mutation on Scrimba.

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 parameter deconstruction to simplify code (especially if we need to call commit many times) :

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

** Action is triggered by the store.dispatch method:

At first glance, store.dispatch(‘increment’) may seem redundant, but wouldn’t it be more convenient to distribute mutation directly? 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:

// Store. Dispatch ('incrementAsync', {amount: 10}) // Distribute store.dispatch({amount: 10})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.

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

import { mapActions } from 'vuex'

exportdefault { // ... 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

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(() => { // ... }) can also be used in another action: actions: {//... actionB ({ dispatch, commit }) {return dispatch('actionA').then(() => {
      commit('someOtherMutation')}}}Copy the code

Finally, if we use async/await, we can combine actions as follows:

// Suppose getData() and getOtherData() return a Promise

actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA'// Wait for actionA to complete the 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 the module **

const moduleA = { state: { count: 0 }, mutations: Count++}}, getters: {doubleCount (state) {increment (state) {increment (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

** Namespaces ** By default, actions, mutations, and getters inside modules 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: {... }, // the state in the module is already nested, and using the 'namespaced' attribute doesn't affect it.isAdmin() {... } // -> getters['account/isAdmin']
      },
      actions: {
        login() {... } // -> dispatch('account/login')
      },
      mutations: {
        login() {... } // -> commit('account/login'}, // the nested module modules: {// inherits the parent module's namespace myPage: {state: {... }, 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.

** If you want to use Global state and getter, rootState and rootGetter are passed into the getter as the third and fourth arguments, Actions are also passed in through properties of the Context object.

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

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

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

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

** When using the mapState, mapGetters, mapActions, and mapMutations functions 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')

exportDefault {computed: {// Look in 'some/nested/module'... MapState ({a: state => state.a, b: state => state.b})}, methods: {// Find in 'some/nested/module'... mapActions(['foo'.'bar'])}}Copy the code

** 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:

// Get the space name from the plug-in’s argument object // then return the Vuex plug-in function

export function createPlugin (options = {}) {
  return function(store) {// Add the space name to the type of the plug-in module (type) to const namespace = options. The namespace | |' '
    store.dispatch(namespace + 'pluginAction')}}Copy the code

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

MyModule store.registerModule(‘myModule’, {//… }) // Register nested module store.registerModule([‘nested’, ‘myModule’], {//… }) can 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 combines vuE-Router and VUex through a dynamic registration module to manage application routing status.

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).

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 (for example, when runInNewContext is false or ‘once’, To avoid stateful singleton in server rendering) register the same module multiple times in a store. If we use a pure object to declare the module’s state, the state object will be shared by reference, leading to the problem of data contamination between stores or modules 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 () {
    return {
      foo: 'bar'}}, // mutation, action and getter... }Copy the code