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 staterootStateAnd global GettersrootGettersWill be the third and fourth arguments
  • For module actions,contextThe object of thecontext.rootStatecontext.rootGettersExpose 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 argumentdispatchcommit
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