So far we have learned how to handle data (or we called state) for a single component (via data properties) or multiple components (by Provide and Inject pattern).
Local State:
Affect one component.
Ex: component instance properties
Global State:
Affect multiple components or entire app.
Ex: authenticate data (JWT)
However, it is really difficult to manage state in global level
1. You might need to add lots of logic in App.vue
2. It is hard to track the state change.
3. It is error-prone for updating state
Then
Vuex can help!
1. Outsourced state management
2. Predictable state management/flow
3. Less errors (clearly defined data flow)
Installation
$ npm install vuex@next
Before using Vuex
It is an basic example to use 'properties' for local state. (Without Vuex)
Ex:
// App.vue
<template>
<the-counter></the-counter>
</template>
<script>
import TheCounter from './components/TheCounter.vue';
export default {
components: {
'the-counter': TheCounter,
},
};
</script>
// components/TheCounter.vue
<template>
<!-- Bind to properties -->
<h1>{{ counter }}</h1>
<button v-on:click="increase">Add 1</button>
</template>
<script>
export default {
// Using properties
data() {
return {
counter: 0,
};
},
methods: {
increase() {
this.counter++;
},
},
};
</script>
Using state
We can use '
createStore' provided by Vuex to create a store for our Vue app.
A 'store' can maintain all the
state (data) in Vue application level.
Ex:
// main.js
import { createApp } from 'vue';
import { createStore } from 'vuex';
import App from './App.vue';
// Create store
const store = createStore({
state() {
return {
counter: 0,
};
}
});
const app = createApp(App);
// Install plugin
app.use(store);
app.mount('#app');
// components/TheCounter.vue
<template>
<!-- Using state in store -->
<h1>{{ $store.state.counter }}</h1>
<button v-on:click="increase">Add 1</button>
</template>
<script>
export default {
methods: {
increase() {
// Update state directly
this.$store.state.counter++;
},
},
};
</script>
Using mutations
According to previous example, it is not ideal to change the state of store directly.
Imagine that if your app get bigger and lots of place change the state directly.
Then it is hard to control and track where and why the state got changed.
We should use
mutations provided by Vuex to update state.
Ex:
// main.js
const store = createStore({
state() {
return {
counter: 0,
};
},
// Define mutations
mutations: {
increase(state) {
state.counter++;
}
}
});
// components/TheCounter.vue
<template>
<h1>{{ $store.state.counter }}</h1>
<button v-on:click="increase">Add 1</button>
</template>
<script>
export default {
methods: {
increase() {
// Send a commit for triggering mutations
this.$store.commit('increase');
},
},
};
</script>
Passing data to mutations is possible
From previous example, we might want to control the increase value when button was clicked.
Refer to
this.
Ex:
// main.js
const store = createStore({
state() {
return {
counter: 0,
};
},
mutations: {
// In most cases, payload should be object (flexibility)
increase(state, payload) {
state.counter += payload.amount;
}
}
});
// components/TheCounter.vue
<template>
<h1>{{ $store.state.counter }}</h1>
<button v-on:click="increase">Add 1</button>
</template>
<script>
export default {
methods: {
increase() {
// It is Object-style commit
this.$store.commit({
type: 'increase',
amount: 5,
});
},
},
};
</script>
Using getters?
If we need to compute a state with others (like the case when using 'computed' in component), we need to copy computing logic to all components if used. Duplicate code is not what we want.
Vuex provide
getter for us.
Ex:
// main.js
const store = createStore({
state() {
return {
counter: 0,
};
},
getters: {
// Computing the final value of counter
finalCounter(state) {
return state.counter * 2;
}
},
mutations: {
increase(state, payload) {
state.counter += payload.amount;
}
}
});
// components/TheCounter.vue
<template>
<!-- Using computed value -->
<h1>{{ counter }}</h1>
<button v-on:click="increase">Add 1</button>
</template>
<script>
export default {
computed: {
counter() {
// Using getter to get the final counter
return this.$store.getters.finalCounter;
},
},
methods: {
increase() {
this.$store.commit({
type: 'increase',
amount: 5,
});
},
},
};
</script>
Using actions
In realistic project, we need to send data to server to update database, and we will update the view depends on the web api response.
And since web api call is asynchronous, we cannot use mutations (it must be synchronous) to update state.
Vuex provide
actions for us.
Ex:
// main.js
const store = createStore({
state() {
return {
counter: 0,
};
},
getters: {
finalCounter(state) {
return state.counter * 2;
}
},
mutations: {
increase(state, payload) {
state.counter += payload.amount;
}
},
actions: {
increase(content, payload) {
// Force to have a delay for exp only
setTimeout(function () {
content.commit('increase', payload)
}, 5000)
}
}
});
// components/TheCounter.vue
<template>
<h1>{{ counter }}</h1>
<button v-on:click="increase">Add 1</button>
</template>
<script>
export default {
computed: {
counter() {
return this.$store.getters.finalCounter;
},
},
methods: {
increase() {
// Trigger actions
this.$store.dispatch({
type: 'increase',
amount: 5,
});
},
},
};
</script>
NOTE:
Refer to
this.
You cat dispatch action to another action!
For example, you can dispatch to succeed/failure actions after web api call actions.
Mapper Helper
So far you might feel that the code is
verbose such as we have used lots of code like 'this.$store.getters.xxx' and 'this.$store.dispatch(xxx)'.
Vuex provides
mapper helper to save your code.
Ex:
// components/TheCounter.vue
<template>
<!-- bind to getters -->
<h1>{{ finalCounter }}</h1>
<!-- bind to actions -->
<button v-on:click="increase({ amount: 5 })">Add 1</button>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
export default {
computed: {
// map helper for getters
...mapGetters(['finalCounter']),
},
methods: {
// map helper for actions
...mapActions(['increase']),
},
};
</script>
Modules
If our app get bigger, you may want to find a way to manage the state based on features.
Below is an example if we have counter and auth features.
Ex:
// main.js
const store = createStore({
state() {
return {
counter: 0,
isLoggedIn: false,
};
},
getters: {
finalCounter(state) {
return state.counter * 2;
},
isUserAuthenticated(state) {
return state.isLoggedIn;
}
},
mutations: {
increase(state, payload) {
state.counter += payload.amount;
},
setAuth(state, payload) {
state.isLoggedIn = payload.isAuth;
}
},
actions: {
increase(content, payload) {
content.commit('increase', payload)
},
login(content) {
content.commit('setAuth', { isAuth: true })
},
logout(content) {
content.commit('setAuth', { isAuth: false })
},
}
});
const app = createApp(App);
app.use(store);
app.mount('#app');
We can use
module to separate it to make the code clean and readable.
Ex:
// main.js
// Counter feature module
const counter = {
state() {
return {
counter: 0,
};
},
getters: {
finalCounter(state) {
return state.counter * 2;
},
},
mutations: {
increase(state, payload) {
state.counter += payload.amount;
},
},
actions: {
increase(content, payload) {
content.commit('increase', payload)
},
},
}
// Root Module
const store = createStore({
// Setup feature modules
modules: {
'counter': counter,
},
state() {
return {
isLoggedIn: false,
};
},
getters: {
isUserAuthenticated(state) {
return state.isLoggedIn;
}
},
mutations: {
setAuth(state, payload) {
state.isLoggedIn = payload.isAuth;
}
},
actions: {
login(content) {
content.commit('setAuth', { isAuth: true })
},
logout(content) {
content.commit('setAuth', { isAuth: false })
},
}
});
const app = createApp(App);
app.use(store);
app.mount('#app');
Note that once you separate the modules, the feature module need to use 'rootState' to access state from root module.
Name clashes?
Refer to
this and
that.
Ex:
// main.js
const counter = {
// Enable namespaced feature
namespaced: true,
state() {
return {
counter: 0,
};
},
getters: {
finalCounter(state) {
return state.counter * 2;
},
},
mutations: {
increase(state, payload) {
state.counter += payload.amount;
},
},
actions: {
increase(content, payload) {
content.commit('increase', payload)
},
},
}
// components/TheCounter.vue
<template>
<!-- bind to getters -->
<h1>{{ finalCounter }}</h1>
<!-- bind to actions -->
<button v-on:click="increase({ amount: 5 })">Add 1</button>
</template>
<script>
import { mapActions } from 'vuex';
export default {
computed: {
// Using getters with namespace feature
finalCounter() {
return this.$store.getters['counter/finalCounter'];
},
},
methods: {
// Using map helper with namespace feature
...mapActions('counter', ['increase']),
},
};
</script>
Structuring
According to previous example, you may notice that main.js get bigger and bigger, and it is hard to manage now.
We can organize our app as below.
// Structure
src
├─── components
├─── store
│ ├─── modules
│ │ └─ counter
│ │ ├── actions.js
│ │ ├── getters.js
│ │ ├── index.js
│ │ └── mutations.js
│ ├─── actions.js
│ ├─── getters.js
│ ├─── index.js
│ └─── mutations.js
├─── App.vue
└─── main.js
Ex:
// main.js
import { createApp } from 'vue';
import App from './App.vue';
import store from './store/index.js';
const app = createApp(App);
// Install plugin
app.use(store);
app.mount('#app');
// store/index.js
import { createStore } from 'vuex';
import counterModule from './modules/counter/index.js';
import rootGetters from './getters.js'
import rootMutations from './mutations.js';
import rootActions from './actions.js';
// Root Modules
export default createStore({
modules: {
// Setup Counter Feature Module
'counter': counterModule,
},
state() {
return {
isLoggedIn: false,
};
},
getters: rootGetters,
mutations: rootMutations,
actions: rootActions,
});
// store/actions.js
export default {
login(content) {
content.commit('setAuth', { isAuth: true })
},
logout(content) {
content.commit('setAuth', { isAuth: false })
},
}
// store/modules/counter/index.js
import counterActions from './actions.js';
import counterGetters from './getters.js';
import counterMutations from './mutations.js';
// Counter Feature Module
export default {
namespaced: true,
state() {
return {
counter: 0,
};
},
getters: counterGetters,
mutations: counterMutations,
actions: counterActions,
}