Recently, I was writing the home page of deepincn, and I saw elementary os’ home page style was quite nice, so I planned to learn from it.

Originally intended to use Angular development, but see Vue3 has been very stable, so I plan to regain Vue3, using Vue3 to write the new home page.

In order to facilitate future data updates, you need to use centralized data management, Vue under the more famous and good is Vuex, and Vuex has also been adapted to Vue3, you can rest assured that the use of.

This article will briefly introduce Vuex, then talk about the basic syntax, I will write another article to specifically draw on a Vuex, through the implementation of a simple Vuex to understand the core.

Introduction

One of the most important feature points of Vue is data-driven .

By data-driven, we mean that the view is generated by data-driven, and we don’t modify the view by directly manipulating the DOM, but by modifying the data. It greatly simplifies the amount of code compared to our traditional front-end development, such as using jQuery and other front-end libraries to directly modify the DOM.

Especially when the interaction is complex, only caring about data modification will make the logic of the code very clear, because the DOM becomes a data mapping, all our logic is to modify the data without touching the DOM, so the code is very easy to maintain.

Another important feature of Vue is componentization, each component is the smallest unit of functionality, each component has its own data, template and methods. We update the view in template by modifying the data in data. It is very convenient to update the view in a single component, but in practice, when we have more than one component in our project, it is very difficult to maintain the same state when many components are integrated together, especially when the embedding is very deep, passing parameters becomes very troublesome (here I think of Deepin’s control center, there are very deep nesting inside the component, and the innermost component (to get the data, you need to pass it along the nesting layers, which is very troublesome).

To solve this problem, we need to introduce Vuex for state management, which is responsible for communication among components.

Problems solved by Vuex

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

Basic Use

First install Vuex in the project, Vuex4 is the version developed for Vue3 and we install it with the following command.

npm

1
npm install vuex@next

yarn

1
yarn add vuex@next

At the heart of every Vuex application is the store. The “store” is basically a container that contains most of the state in your application.

Vuex’s state store is responsive. When a Vue component reads state from the store, if the state in the store changes, the corresponding component will be updated accordingly and efficiently. 2. You can’t change the state in store directly. The only way to change the state in store is to explicitly commit mutation, which makes it easy to track every change in state and allows us to implement tools that help us better understand our application.

Now that Vuex is installed, we can start writing a simple store by creating a store.ts file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { createStore } from 'vuex'

const store = createStore({
    state: {
        count: 0
    },
    mutations: {
        increment (state) {
            state.count++
        }
    },
});

export { store };

In main.ts, register the store to the app instance of Vue.

1
2
3
4
5
6
7
import { store } from './store'

...

app.use(store);

app.mount('#app'); // Add before mounting

The above is the simplest example of a store, is it a little confusing?

In store.ts we export the object returned using the createStore function, and in store we define two properties: state and mutations.

We define the state in state and the operation methods in mutations. The methods defined in mutations must be called by the store.commit() method and cannot be accessed directly.

Now let’s use store in our component.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<template>
    <div>
        <p>{{ count }}</p>
    </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({

    setup() {
        return { count: this.$store.state.count };
    },
    methods: {
        increment() {
            this.$store.commit('increment')
        }
    }
});

In the component example, the setup function is a composition API introduced in Vue3, so we won’t go into detail about the use of setup here.

In the example, setup provides the count variable to the view, here the assignment is deconstructed, when the template uses the count variable, it actually accesses the count in the store, why?

This is because Vuex’s state store is responsive.

In the example, the component provides a method to modify the count in the store, and the only way to change the state in Vuex’s store is to commit a mutation. mutations in Vuex are very similar to events: each mutation has a string event type and a callback function (handler ). This callback function is where we actually make the state change, and it accepts state as the first argument.

You can’t call a mutation handler directly. This option is more like an event registration: “Call this function when a mutation of type increment is triggered.” .

Core Concepts

Vuex has a total of five core concepts: State, Getter, Mutation, Action and Module.

State

Vuex uses a single state tree that contains all the application-level state in a single object. It then exists as a “unique data source (SSOT)”. This also means that each application will contain only one instance of store. The single state tree allows us to directly locate any particular state fragment and easily get a snapshot of the entire current application state during debugging.

In the component, we can fetch the registered state via this.$store.state, and whenever store.state.count changes, the computed property is retrieved and the associated DOM is triggered to be updated.

Vuex “injects” store instances from the root component into all child components via Vue’s plugin system. The child components can be accessed through this.$store.

template can be used directly with this.$store.state.[property] and this can be omitted.

1
2
3
4
5
6
<template>
  <div id="app">
    {{ this.$store.state.name }}
    {{ this.$store.state.age }}
  </div>
</template>

Getter

Sometimes we need to derive some state from the state in the store, for example to filter and count lists.

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

If there are multiple components that need to use this property, we either have to copy the function or extract it to a shared function and import it in multiple places - either way is not ideal.

Vuex allows us to define a “getter” (think of it as a computed property of the store) in the store.

The getter accepts state as its first argument.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const store = createStore({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos (state) => {
      return state.todos.filter(todo => todo.done)
    }
  }
})

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

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

Mutation

As mentioned above, the only way to change the state in Vuex’s store is to commit mutation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const store = createStore({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      // Change Status
      state.count++
    }
  }
})

The commit method is required to commit a call.

1
store.commit('increment')

Multiple parameters can also be submitted, and the commit function will expand all parameters:

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

Action

Action is similar to mutation, except that

  1. an Action commits a mutation instead of changing state directly.
  2. Action can contain any asynchronous operation.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const store = createStore({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

The Action function accepts a context object with the same methods and properties as the store instance, so you can call context.commit to submit a mutation, or context.state and context.getters to get the state and getters.

Action is triggered by the store.dispatch method.

1
store.dispatch('increment')

At first glance, it seems superfluous. Wouldn’t it be easier to just distribute the mutation? Action is not bound by this restriction! We can perform asynchronous operations inside the action.

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

Module

Because of the use of a single state tree, all the state of the application is concentrated into one relatively large object. When the application becomes very complex, the store object can become quite bloated.

To solve this problem, Vuex allows us to split the store into modules. Each module has its own state, mutation, action, getter, and even nested submodules - split in the same way from top to bottom: the

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}

const store = createStore({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> Status of moduleA
store.state.b // -> Status of moduleB

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const moduleA = {
  state: () => ({
    count: 0
  }),
  mutations: {
    increment (state) {
      // Here the `state` object is the local state of the module
      state.count++
    }
  },

  getters: {
    doubleCount (state) {
      return state.count * 2
    }
  }
}

Similarly, for actions inside the module, the local state is exposed through context.state, and the root state is context.rootState.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const moduleA = {
  // ...
  actions: {
    incrementIfOddOnRootSum ({ state, commit, rootState }) {
      if ((state.count + rootState.count) % 2 === 1) {
        commit('increment')
      }
    }
  }
}

By default, actions and mutations inside modules are still registered in the global namespace - this allows multiple modules to respond to the same action or mutation. getter is also registered in the global namespace by default, but this is not currently the case for functional purpose (just maintaining the status quo to avoid non-compatible changes). Care must be taken not to define two different getters in different, namespace-less modules, leading to errors.

If you want your module to be more encapsulated and reusable, you can make it namespaced by adding namespaced: true. When a module is registered, all its getters, actions and mutations are automatically namespaced according to the path of the module registration. Example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const store = createStore({
  modules: {
    account: {
      namespaced: true,

      // Module assets
      state: () => ({ ... }), // The state within the module is already nested, and using the `namespaced` attribute will have no effect on it
      getters: {
        isAdmin () { ... } // -> getters['account/isAdmin']
      },
      actions: {
        login () { ... } // -> dispatch('account/login')
      },
      mutations: {
        login () { ... } // -> commit('account/login')
      },

      // Nested Modules
      modules: {
        // Inherit the namespace of the parent module
        myPage: {
          state: () => ({ ... }),
          getters: {
            profile () { ... } // -> getters['account/profile']
          }
        },

        // Further nested namespaces
        posts: {
          namespaced: true,

          state: () => ({ ... }),
          getters: {
            popular () { ... } // -> getters['account/posts/popular']
          }
        }
      }
    }
  }
})

Namespace-enabled getters and actions receive localized getters, dispatches, and commits; in other words, you don’t need to add additional spatial prefixes within the same module when using module assets. Changing the namespaced attribute does not require any changes to the code within the module.

with TypeScript

Vuex4 still has some trouble using TypeScript, and Vue provides an InjectionKey interface, which is a generic type that extends Symbol. It can be used to synchronize the type of the inject value between the provider and the consumer.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import { createStore, Store } from 'vuex'
import { InjectionKey } from 'vue'
import { Menu } from './settings';

export interface Menu {
    icon?: string;
    text: string;
    url: string;
    id: string;
}

interface MenuState {
  menus: Menu[];
}

const MenuKey: InjectionKey<Store<MenuState>> = Symbol()

const menuStore = createStore<MenuState>({
  state: {
    menus: []
  },
  mutations: {
    add(state: MenuState, menu: Menu) {
      state.menus.push(menu);
    }
  },
  actions: {
    add(context, menu: Menu) {
      context.commit('add', menu);
    }
  },
  getters: {
    menus(state) {
      return state.menus;
    }
  }
})

export { Menu, MenuKey, menuStore };

Inject the dependency in main.ts.

1
2
3
4
5
6
import { MenuKey, menuStore } from './store'
// ...

app.use(menuStore, MenuKey);

// ...

Reference https://blog.justforlxz.com/2021/08/10/Vuex%E5%9F%BA%E6%9C%AC%E4%BD%BF%E7%94%A8/