VDone Demo VDone Demo
Home
  • Articles

    • JavaScript
  • Study Notes

    • JavaScript Tutorial
    • Professional JavaScript
    • ES6 Tutorial
    • Vue
    • React
    • TypeScript: Build Axios from Scratch
    • Git
    • TypeScript
    • JS Design Patterns
  • HTML
  • CSS
  • Technical Docs
  • GitHub Tips
  • Node.js
  • Blog Setup
  • Learning
  • Interviews
  • Miscellaneous
  • Practical Tips
  • Friends
About
Bookmarks
  • Categories
  • Tags
  • Archives
GitHub (opens new window)

Nikolay Tuzov

Backend Developer
Home
  • Articles

    • JavaScript
  • Study Notes

    • JavaScript Tutorial
    • Professional JavaScript
    • ES6 Tutorial
    • Vue
    • React
    • TypeScript: Build Axios from Scratch
    • Git
    • TypeScript
    • JS Design Patterns
  • HTML
  • CSS
  • Technical Docs
  • GitHub Tips
  • Node.js
  • Blog Setup
  • Learning
  • Interviews
  • Miscellaneous
  • Practical Tips
  • Friends
About
Bookmarks
  • Categories
  • Tags
  • Archives
GitHub (opens new window)
  • 基础

  • 组件

  • 过渡&动画

  • 可复用性&组合

  • 工具

  • 规模化

  • Vuex

    • Vuex
      • Vuex Usage Demo
      • Core Concepts
        • State
        • mapState Helper
        • Object Spread Operator
        • Components Can Still Have Local State
        • Getter
        • Property-Style Access
        • Method-Style Access
        • mapGetters Helper
        • Mutation
        • Commit with Payload
        • Object-Style Commit
        • Mutations Follow Vue's Reactivity Rules
        • Using Constants for Mutation Types
        • Mutations Must Be Synchronous
        • Committing Mutations in Components (mapMutations Helper)
        • Next: Action
        • Action
        • Dispatching Actions
        • Dispatching Actions in Components (mapActions Helper)
        • Composing Actions
      • More
  • 其他

  • 《Vue》笔记
  • Vuex
xugaoyi
2020-08-08
Contents

Vuex

# Vuex

Vuex is a state management pattern developed specifically for Vue.js applications.

# Vuex Usage Demo

Using a project newly created with vue-cli3 as an example to demonstrate the Vuex usage process.

Create a project:

vue create vuex-test
cd vuex-test
npm run serve
1
2
3

Install vuex:

npm i vuex -S
1

Navigate to the project's src/ directory and create a new file store/index.js with the following content:

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({ // Store container (constructor Store starts with uppercase)
    state: { // State
        count: 0
    },
    mutations: { // Mutations (using mutations to commit changes makes it easy to track change history)
        increment (state){
            state.count++
        }
    }
})
export default store // Export
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

Go to main.js and inject store to make all Vue components able to use Vuex:

// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')
1
2
3
4
5
6
7
8
9
10
11
12
13

Now we can commit a change from a component's method:

methods: {
  increment() {
    this.$store.commit('increment') // .commit('<mutation event name>')
    console.log(this.$store.state.count)
  }
}
1
2
3
4
5
6

Using state in a component template:

{{ count }}

computed: {
	count() {
		return this.$store.state.count
	}
}
1
2
3
4
5
6
7

State changes trigger recomputation of computed properties

# Core Concepts

# State

Vuex uses a single state tree -- each application will contain only one store instance.

# mapState Helper

When a component needs to access multiple states, declaring all of them as computed properties can be repetitive and verbose. To solve this problem, we can use the mapState helper to generate computed properties, saving you some keystrokes:

// In a standalone build, the helper is available as Vuex.mapState
import { mapState } from 'vuex'

export default {
  // ...
  computed: mapState({
    // Arrow function for concise code
    count: state => state.count,

    // Passing the string 'count' is equivalent to `state => state.count`
    countAlias: 'count',

    // To use `this` to access local state, a regular function must be used
    countPlusLocalState (state) {
      return state.count + this.localCount
    }
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

All three approaches in the mapState() parameter above are ways to get the count value.

When the mapped computed property name matches a state sub-tree node name, we can also pass a string array to mapState.

computed: mapState([
  // Maps this.count to store.state.count
  'count'
])
1
2
3
4

# Object Spread Operator

mapState returns an object. We can use the object spread operator to mix it into the outer object:

computed: {
  localComputed () { /* ... */ }, // other computed properties
  // Use the object spread operator to mix this object into the outer object
  ...mapState({
    // ... same getter methods as above
  })
}
1
2
3
4
5
6
7

# Components Can Still Have Local State

Using Vuex doesn't mean you should put all your state in Vuex. Although putting all state in Vuex makes state changes more explicit and easy to debug, it would also make the code verbose and unintuitive. If some state strictly belongs to a single component, it's fine to keep it as local state. You should weigh the trade-offs based on your application's development needs.

# Getter

Sometimes we need to derive some state from the store's state, for example filtering through a list and counting:

computed: {
  doneTodosCount () {
    return this.$store.state.todos.filter(todo => todo.done).length
  }
}
1
2
3
4
5

If more than one component needs to use this property, we either duplicate this function, or extract it into a shared helper and import it in multiple places -- neither is ideal.

Vuex allows us to define "getters" in the store (think of them as computed properties for stores). Like computed properties, a getter's result is cached based on its dependencies, and will only re-evaluate when some of its dependencies have changed.

Getters receive state as their 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)
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13

# Property-Style Access

Getters will be exposed on the store.getters object, and you access values as properties:

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

Getters can also accept other getters as the second argument:

getters: {
  // ...
  doneTodosCount: (state, getters) => {
    return getters.doneTodos.length
  }
}
store.getters.doneTodosCount // -> 1
1
2
3
4
5
6
7

We can easily use it inside any component:

computed: {
  doneTodosCount () {
    return this.$store.getters.doneTodosCount
  }
}
1
2
3
4
5

Note that getters accessed as properties are cached as part of Vue's reactivity system.

# Method-Style Access

You can also pass arguments to getters by making the getter return a function. This is particularly useful when querying an array in the store.

getters: {
  // ...
  getTodoById: (state) => (id) => {
    return state.todos.find(todo => todo.id === id)
  }
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
1
2
3
4
5
6
7

Note that getters accessed via methods will run each time they are called, and the result is not cached.

# mapGetters Helper

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

import { mapGetters } from 'vuex'

export default {
  // ...
  computed: {
  // Use the object spread operator to mix getters into the computed object
    ...mapGetters([
      'doneTodosCount',
      'anotherGetter',
      // ...
    ])
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

If you want to map a getter to a different name, use an object:

...mapGetters({
  // Map `this.doneCount` to `this.$store.getters.doneTodosCount`
  doneCount: 'doneTodosCount'
})
1
2
3
4

# Mutation

The only way to change state in a Vuex store is by committing a mutation. Vuex mutations are very similar to events: each mutation has a string event type and a handler function. The handler function is where we perform actual state modifications, and it receives state as the first argument:

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      // mutate state
      state.count++
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11

You cannot directly call a mutation handler. Think of it more like event registration: "When a mutation with type increment is triggered, call this handler." To invoke a mutation handler, you need to call store.commit with its type:

store.commit('increment')
1

# Commit with Payload

You can pass additional arguments to store.commit, which is the mutation's payload:

// ...
mutations: {
  increment (state, n) {
    state.count += n
  }
}
store.commit('increment', 10)
1
2
3
4
5
6
7

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

// ...
mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}
store.commit('increment', {
  amount: 10
})
1
2
3
4
5
6
7
8
9

# Object-Style Commit

An alternative way to commit a mutation is by directly using an object with a type property:

store.commit({
  type: 'increment',
  amount: 10
})
1
2
3
4

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

mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}
1
2
3
4
5

# Mutations Follow Vue's Reactivity Rules

Since the Vuex store's state is reactive, when we mutate the state, Vue components observing the state will update automatically. This also means Vuex mutations need to follow the same caveats as working with Vue:

  1. Prefer to initialize all needed state properties upfront in your store.
  2. When adding new properties to an Object, you should either:
  • Use Vue.set(obj, 'newProp', 123), or

  • Replace the object with a fresh one. For example, using the object spread operator (opens new window) we can write:

    state.obj = { ...state.obj, newProp: 123 }
    
    1

# Using Constants for Mutation Types

Using constants for mutation types is a common pattern in various Flux implementations. This allows linter tools to work effectively, and putting these constants in a separate file gives collaborators an at-a-glance view of all mutations 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 the ES2015 computed property name feature to use a constant as the function name
    [SOME_MUTATION] (state) {
      // mutate state
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Whether to use constants is up to you -- it can be helpful in large projects where many people collaborate. But if you don't like them, you are free to skip this.

# Mutations Must Be Synchronous

One important rule is to remember that mutations must be synchronous functions. Why? Consider this example:

mutations: {
  someMutation (state) {
    api.callAsyncMethod(() => {
      state.count++
    })
  }
}
1
2
3
4
5
6
7

Now imagine we are debugging an app and looking at the mutation log in devtools. For every mutation logged, devtools needs to capture a "before" and "after" snapshot of the state. However, the asynchronous callback in the mutation above makes this impossible: the callback is not yet called when the mutation is triggered, and devtools has no way of knowing when the callback is actually called -- making any state mutation performed in the callback essentially untrackable.

# Committing Mutations in Components (mapMutations Helper)

You can commit mutations in components with this.$store.commit('xxx'), or use the mapMutations helper to map component methods to store.commit calls (requires injecting store into the root component).

import { mapMutations } from 'vuex'

export default {
  // ...
  methods: {
    ...mapMutations([
      'increment', // Maps `this.increment()` to `this.$store.commit('increment')`

      // `mapMutations` also supports payloads:
      'incrementBy' // Maps `this.incrementBy(amount)` to `this.$store.commit('incrementBy', amount)`
    ]),
    ...mapMutations({
      add: 'increment' // Maps `this.add()` to `this.$store.commit('increment')`
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# Next: Action

Mixing asynchronous calls in mutations can make your program very hard to debug. For example, when you call two mutations that both have async callbacks to mutate state, how do you know when they are called and which callback was called first? This is why we separate the two concepts. In Vuex, mutations are synchronous transactions:

store.commit('increment')
// Any state change that "increment" causes should be completed at this moment.
1
2

To handle asynchronous operations, let's look at Actions (opens new window).

# Action

Actions are similar to mutations, with the differences being:

  • Actions commit mutations, rather than directly mutating the state.
  • Actions can contain arbitrary asynchronous operations.

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')
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Action functions receive a context object with the same methods and properties as the store instance, so you can call context.commit to commit a mutation, or access state and getters via context.state and context.getters. When we introduce Modules (opens new window) later, you'll see why the context object is not the store instance itself.

In practice, we often use ES2015 argument destructuring (opens new window) to simplify the code (especially when we need to call commit many times):

actions: {
  increment ({ commit }) {
    commit('increment')
  }
}
1
2
3
4
5

# Dispatching Actions

Actions are triggered via the store.dispatch method:

store.dispatch('increment')
1

This might seem like extra work at first glance -- wouldn't it be more convenient to just dispatch mutations directly? Remember that mutations must be synchronous. Actions don't have that constraint! We can perform asynchronous operations inside an action:

actions: {
  incrementAsync ({ commit }) {
    setTimeout(() => {
      commit('increment')
    }, 1000)
  }
}
1
2
3
4
5
6
7

Actions support the same payload format and object-style dispatch:

// Dispatch with a payload
store.dispatch('incrementAsync', {
  amount: 10
})

// Dispatch with an object
store.dispatch({
  type: 'incrementAsync',
  amount: 10
})
1
2
3
4
5
6
7
8
9
10

A more practical example of a shopping cart, which involves calling async APIs and committing multiple mutations:

actions: {
  checkout ({ commit, state }, products) {
    // Save the items currently in the cart
    const savedCartItems = [...state.cart.added]
    // Send checkout request, then optimistically clear the cart
    commit(types.CHECKOUT_REQUEST)
    // The shop API accepts a success callback and a failure callback
    shop.buyProducts(
      products,
      // Success handler
      () => commit(types.CHECKOUT_SUCCESS),
      // Failure handler
      () => commit(types.CHECKOUT_FAILURE, savedCartItems)
    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Note that we are performing a series of asynchronous operations and recording the side effects (state mutations) of the action by committing mutations.

# Dispatching Actions in Components (mapActions Helper)

You can dispatch actions in components with this.$store.dispatch('xxx'), or use the mapActions helper to map component methods to store.dispatch calls (requires injecting store into the root component first):

import { mapActions } from 'vuex'

export default {
  // ...
  methods: {
    ...mapActions([
      'increment', // Maps `this.increment()` to `this.$store.dispatch('increment')`

      // `mapActions` also supports payloads:
      'incrementBy' // Maps `this.incrementBy(amount)` to `this.$store.dispatch('incrementBy', amount)`
    ]),
    ...mapActions({
      add: 'increment' // Maps `this.add()` to `this.$store.dispatch('increment')`
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# Composing Actions

Actions are often asynchronous, so how do we know when an action is done? And more importantly, how can we compose multiple actions to handle more complex async flows?

First, you need to understand that store.dispatch can handle the Promise returned by the triggered action's handler, and that store.dispatch itself also returns a Promise:

actions: {
  actionA ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('someMutation')
        resolve()
      }, 1000)
    })
  }
}
1
2
3
4
5
6
7
8
9
10

Now you can do:

store.dispatch('actionA').then(() => {
  // ...
})
1
2
3

And also in another action:

actions: {
  // ...
  actionB ({ dispatch, commit }) {
    return dispatch('actionA').then(() => {
      commit('someOtherMutation')
    })
  }
}
1
2
3
4
5
6
7
8

Finally, if we use async / await (opens new window), we can compose actions like this:

// Assuming getData() and getOtherData() return Promises

actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA') // Wait for actionA to finish
    commit('gotOtherData', await getOtherData())
  }
}
1
2
3
4
5
6
7
8
9
10
11

A store.dispatch can trigger multiple action functions in different modules. In such a case, the returned Promise will only resolve when all triggered handlers have been resolved.

# More

For more content, see the official documentation: https://vuex.vuejs.org/zh/guide/modules.html (opens new window)

Edit (opens new window)
Last Updated: 2026/03/21, 12:14:36
Route Lazy Loading
Debounce Function in Vue - Implementation and Usage

← Route Lazy Loading Debounce Function in Vue - Implementation and Usage→

Recent Updates
01
How I Discovered Disposable Email — A True Story
06-12
02
Animations in Grid Layout
09-15
03
Renaming a Git Branch
08-11
More Articles >
Theme by VDone | Copyright © 2026-2026 Nikolay Tuzov | MIT License | Telegram
  • Auto
  • Light Mode
  • Dark Mode
  • Reading Mode