Merge branch '44427-state-management-with-vuex' into 'master'
Resolve "Vuex actions - How to achieve a better state management" Closes #44427 See merge request gitlab-org/gitlab-ce!17929
This commit is contained in:
commit
2d84de9ec9
3 changed files with 390 additions and 249 deletions
|
@ -45,6 +45,9 @@ Common JavaScript design patterns in GitLab's codebase.
|
||||||
## [Vue.js Best Practices](vue.md)
|
## [Vue.js Best Practices](vue.md)
|
||||||
Vue specific design patterns and practices.
|
Vue specific design patterns and practices.
|
||||||
|
|
||||||
|
## [Vuex](vuex.md)
|
||||||
|
Vuex specific design patterns and practices.
|
||||||
|
|
||||||
## [Axios](axios.md)
|
## [Axios](axios.md)
|
||||||
Axios specific practices and gotchas.
|
Axios specific practices and gotchas.
|
||||||
|
|
||||||
|
|
|
@ -1,29 +1,7 @@
|
||||||
# Vue
|
# Vue
|
||||||
|
|
||||||
For more complex frontend features, we recommend using Vue.js. It shares
|
|
||||||
some ideas with React.js as well as Angular.
|
|
||||||
|
|
||||||
To get started with Vue, read through [their documentation][vue-docs].
|
To get started with Vue, read through [their documentation][vue-docs].
|
||||||
|
|
||||||
## When to use Vue.js
|
|
||||||
|
|
||||||
We recommend using Vue for more complex features. Here are some guidelines for when to use Vue.js:
|
|
||||||
|
|
||||||
- If you are starting a new feature or refactoring an old one that highly interacts with the DOM;
|
|
||||||
- For real time data updates;
|
|
||||||
- If you are creating a component that will be reused elsewhere;
|
|
||||||
|
|
||||||
## When not to use Vue.js
|
|
||||||
|
|
||||||
We don't want to refactor all GitLab frontend code into Vue.js, here are some guidelines for
|
|
||||||
when not to use Vue.js:
|
|
||||||
|
|
||||||
- Adding or changing static information;
|
|
||||||
- Features that highly depend on jQuery will be hard to work with Vue.js;
|
|
||||||
- Features without reactive data;
|
|
||||||
|
|
||||||
As always, the Frontend Architectural Experts are available to help with any Vue or JavaScript questions.
|
|
||||||
|
|
||||||
## Vue architecture
|
## Vue architecture
|
||||||
|
|
||||||
All new features built with Vue.js must follow a [Flux architecture][flux].
|
All new features built with Vue.js must follow a [Flux architecture][flux].
|
||||||
|
@ -57,15 +35,15 @@ new_feature
|
||||||
│ └── ...
|
│ └── ...
|
||||||
├── stores
|
├── stores
|
||||||
│ └── new_feature_store.js
|
│ └── new_feature_store.js
|
||||||
├── services
|
├── services # only when not using vuex
|
||||||
│ └── new_feature_service.js
|
│ └── new_feature_service.js
|
||||||
├── new_feature_bundle.js
|
├── index.js
|
||||||
```
|
```
|
||||||
_For consistency purposes, we recommend you to follow the same structure._
|
_For consistency purposes, we recommend you to follow the same structure._
|
||||||
|
|
||||||
Let's look into each of them:
|
Let's look into each of them:
|
||||||
|
|
||||||
### A `*_bundle.js` file
|
### A `index.js` file
|
||||||
|
|
||||||
This is the index file of your new feature. This is where the root Vue instance
|
This is the index file of your new feature. This is where the root Vue instance
|
||||||
of the new feature should be.
|
of the new feature should be.
|
||||||
|
@ -144,30 +122,30 @@ in one table would not be a good use of this pattern.
|
||||||
You can read more about components in Vue.js site, [Component System][component-system]
|
You can read more about components in Vue.js site, [Component System][component-system]
|
||||||
|
|
||||||
#### Components Gotchas
|
#### Components Gotchas
|
||||||
1. Using SVGs in components: To use an SVG in a template we need to make it a property we can access through the component.
|
1. Using SVGs icons in components: To use an SVG icon in a template use the `icon.vue`
|
||||||
A `prop` and a property returned by the `data` functions require `vue` to set a `getter` and a `setter` for each of them.
|
1. Using SVGs illustrations in components: To use an SVG illustrations in a template provide the path as a prop and display it through a standard img tag.
|
||||||
The SVG should be a computed property in order to improve performance, note that computed properties are cached based on their dependencies.
|
```javascript
|
||||||
|
<script>
|
||||||
```javascript
|
export default {
|
||||||
// bad
|
props: {
|
||||||
import svg from 'svg.svg';
|
svgIllustrationPath: {
|
||||||
data() {
|
type: String,
|
||||||
return {
|
required: true,
|
||||||
myIcon: svg,
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
<script>
|
||||||
|
<template>
|
||||||
// good
|
<img :src="svgIllustrationPath" />
|
||||||
import svg from 'svg.svg';
|
</template>
|
||||||
computed: {
|
```
|
||||||
myIcon() {
|
|
||||||
return svg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### A folder for the Store
|
### A folder for the Store
|
||||||
|
|
||||||
|
#### Vuex
|
||||||
|
Check this [page](vuex.md) for more details.
|
||||||
|
|
||||||
|
#### Flux like state management
|
||||||
The Store is a class that allows us to manage the state in a single
|
The Store is a class that allows us to manage the state in a single
|
||||||
source of truth. It is not aware of the service or the components.
|
source of truth. It is not aware of the service or the components.
|
||||||
|
|
||||||
|
@ -176,6 +154,8 @@ itself, please read this guide: [State Management][state-management]
|
||||||
|
|
||||||
### A folder for the Service
|
### A folder for the Service
|
||||||
|
|
||||||
|
**If you are using Vuex you won't need this step**
|
||||||
|
|
||||||
The Service is a class used only to communicate with the server.
|
The Service is a class used only to communicate with the server.
|
||||||
It does not store or manipulate any data. It is not aware of the store or the components.
|
It does not store or manipulate any data. It is not aware of the store or the components.
|
||||||
We use [axios][axios] to communicate with the server.
|
We use [axios][axios] to communicate with the server.
|
||||||
|
@ -273,6 +253,9 @@ import Store from 'store';
|
||||||
import Service from 'service';
|
import Service from 'service';
|
||||||
import TodoComponent from 'todoComponent';
|
import TodoComponent from 'todoComponent';
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
todo: TodoComponent,
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* Although most data belongs in the store, each component it's own state.
|
* Although most data belongs in the store, each component it's own state.
|
||||||
* We want to show a loading spinner while we are fetching the todos, this state belong
|
* We want to show a loading spinner while we are fetching the todos, this state belong
|
||||||
|
@ -291,10 +274,6 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
components: {
|
|
||||||
todo: TodoComponent,
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.service = new Service('todos');
|
this.service = new Service('todos');
|
||||||
|
|
||||||
|
@ -476,201 +455,6 @@ need to test the rendered output. [Vue][vue-test] guide's to unit test show us e
|
||||||
Refer to [mock axios](axios.md#mock-axios-response-on-tests)
|
Refer to [mock axios](axios.md#mock-axios-response-on-tests)
|
||||||
|
|
||||||
|
|
||||||
## Vuex
|
|
||||||
To manage the state of an application you may use [Vuex][vuex-docs].
|
|
||||||
|
|
||||||
_Note:_ All of the below is explained in more detail in the official [Vuex documentation][vuex-docs].
|
|
||||||
|
|
||||||
### Separation of concerns
|
|
||||||
Vuex is composed of State, Getters, Mutations, Actions and Modules.
|
|
||||||
|
|
||||||
When a user clicks on an action, we need to `dispatch` it. This action will `commit` a mutation that will change the state.
|
|
||||||
_Note:_ The action itself will not update the state, only a mutation should update the state.
|
|
||||||
|
|
||||||
#### File structure
|
|
||||||
When using Vuex at GitLab, separate this concerns into different files to improve readability. If you can, separate the Mutation Types as well:
|
|
||||||
|
|
||||||
```
|
|
||||||
└── store
|
|
||||||
├── index.js # where we assemble modules and export the store
|
|
||||||
├── actions.js # actions
|
|
||||||
├── mutations.js # mutations
|
|
||||||
├── getters.js # getters
|
|
||||||
└── mutation_types.js # mutation types
|
|
||||||
```
|
|
||||||
The following examples show an application that lists and adds users to the state.
|
|
||||||
|
|
||||||
##### `index.js`
|
|
||||||
This is the entry point for our store. You can use the following as a guide:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import Vue from 'vue';
|
|
||||||
import Vuex from 'vuex';
|
|
||||||
import * as actions from './actions';
|
|
||||||
import * as getters from './getters';
|
|
||||||
import mutations from './mutations';
|
|
||||||
|
|
||||||
Vue.use(Vuex);
|
|
||||||
|
|
||||||
export default new Vuex.Store({
|
|
||||||
actions,
|
|
||||||
getters,
|
|
||||||
mutations,
|
|
||||||
state: {
|
|
||||||
users: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
```
|
|
||||||
_Note:_ If the state of the application is too complex, an individual file for the state may be better.
|
|
||||||
|
|
||||||
##### `actions.js`
|
|
||||||
An action commits a mutation. In this file, we will write the actions that will commit the respective mutation:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import * as types from './mutation_types';
|
|
||||||
|
|
||||||
export const addUser = ({ commit }, user) => {
|
|
||||||
commit(types.ADD_USER, user);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
To dispatch an action from a component, use the `mapActions` helper:
|
|
||||||
```javascript
|
|
||||||
import { mapActions } from 'vuex';
|
|
||||||
|
|
||||||
{
|
|
||||||
methods: {
|
|
||||||
...mapActions([
|
|
||||||
'addUser',
|
|
||||||
]),
|
|
||||||
onClickUser(user) {
|
|
||||||
this.addUser(user);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
##### `getters.js`
|
|
||||||
Sometimes we may need to get derived state based on store state, like filtering for a specific prop. This can be done through the `getters`:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// get all the users with pets
|
|
||||||
export getUsersWithPets = (state, getters) => {
|
|
||||||
return state.users.filter(user => user.pet !== undefined);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
To access a getter from a component, use the `mapGetters` helper:
|
|
||||||
```javascript
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
|
|
||||||
{
|
|
||||||
computed: {
|
|
||||||
...mapGetters([
|
|
||||||
'getUsersWithPets',
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
##### `mutations.js`
|
|
||||||
The only way to actually change state in a Vuex store is by committing a mutation.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import * as types from './mutation_types';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
[types.ADD_USER](state, user) {
|
|
||||||
state.users.push(user);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
##### `mutations_types.js`
|
|
||||||
From [vuex mutations docs][vuex-mutations]:
|
|
||||||
> It is a commonly seen pattern to use constants for mutation types in various Flux implementations. This allows the code to take advantage of tooling like linters, and putting all constants in a single file allows your collaborators to get an at-a-glance view of what mutations are possible in the entire application.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
export const ADD_USER = 'ADD_USER';
|
|
||||||
```
|
|
||||||
|
|
||||||
### How to include the store in your application
|
|
||||||
The store should be included in the main component of your application:
|
|
||||||
```javascript
|
|
||||||
// app.vue
|
|
||||||
import store from 'store'; // it will include the index.js file
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'application',
|
|
||||||
store,
|
|
||||||
...
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Vuex Gotchas
|
|
||||||
1. Avoid calling a mutation directly. Always use an action to commit a mutation. Doing so will keep consistency through out the application. From Vuex docs:
|
|
||||||
|
|
||||||
> why don't we just call store.commit('action') directly? Well, remember that mutations must be synchronous? Actions aren't. We can perform asynchronous operations inside an action.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// component.vue
|
|
||||||
|
|
||||||
// bad
|
|
||||||
created() {
|
|
||||||
this.$store.commit('mutation');
|
|
||||||
}
|
|
||||||
|
|
||||||
// good
|
|
||||||
created() {
|
|
||||||
this.$store.dispatch('action');
|
|
||||||
}
|
|
||||||
```
|
|
||||||
1. When possible, use mutation types instead of hardcoding strings. It will be less error prone.
|
|
||||||
1. The State will be accessible in all components descending from the use where the store is instantiated.
|
|
||||||
|
|
||||||
### Testing Vuex
|
|
||||||
#### Testing Vuex concerns
|
|
||||||
Refer to [vuex docs][vuex-testing] regarding testing Actions, Getters and Mutations.
|
|
||||||
|
|
||||||
#### Testing components that need a store
|
|
||||||
Smaller components might use `store` properties to access the data.
|
|
||||||
In order to write unit tests for those components, we need to include the store and provide the correct state:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
//component_spec.js
|
|
||||||
import Vue from 'vue';
|
|
||||||
import store from './store';
|
|
||||||
import component from './component.vue'
|
|
||||||
|
|
||||||
describe('component', () => {
|
|
||||||
let vm;
|
|
||||||
let Component;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
Component = Vue.extend(issueActions);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vm.$destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show a user', () => {
|
|
||||||
const user = {
|
|
||||||
name: 'Foo',
|
|
||||||
age: '30',
|
|
||||||
};
|
|
||||||
|
|
||||||
// populate the store
|
|
||||||
store.dispatch('addUser', user);
|
|
||||||
|
|
||||||
vm = new Component({
|
|
||||||
store,
|
|
||||||
propsData: props,
|
|
||||||
}).$mount();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
[vue-docs]: http://vuejs.org/guide/index.html
|
[vue-docs]: http://vuejs.org/guide/index.html
|
||||||
[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards
|
[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards
|
||||||
[environments-table]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/environments
|
[environments-table]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/environments
|
||||||
|
@ -681,9 +465,5 @@ describe('component', () => {
|
||||||
[vue-test]: https://vuejs.org/v2/guide/unit-testing.html
|
[vue-test]: https://vuejs.org/v2/guide/unit-testing.html
|
||||||
[issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6
|
[issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6
|
||||||
[flux]: https://facebook.github.io/flux
|
[flux]: https://facebook.github.io/flux
|
||||||
[vuex-docs]: https://vuex.vuejs.org
|
|
||||||
[vuex-structure]: https://vuex.vuejs.org/en/structure.html
|
|
||||||
[vuex-mutations]: https://vuex.vuejs.org/en/mutations.html
|
|
||||||
[vuex-testing]: https://vuex.vuejs.org/en/testing.html
|
|
||||||
[axios]: https://github.com/axios/axios
|
[axios]: https://github.com/axios/axios
|
||||||
[axios-interceptors]: https://github.com/axios/axios#interceptors
|
[axios-interceptors]: https://github.com/axios/axios#interceptors
|
||||||
|
|
358
doc/development/fe_guide/vuex.md
Normal file
358
doc/development/fe_guide/vuex.md
Normal file
|
@ -0,0 +1,358 @@
|
||||||
|
# Vuex
|
||||||
|
To manage the state of an application you should use [Vuex][vuex-docs].
|
||||||
|
|
||||||
|
_Note:_ All of the below is explained in more detail in the official [Vuex documentation][vuex-docs].
|
||||||
|
|
||||||
|
## Separation of concerns
|
||||||
|
Vuex is composed of State, Getters, Mutations, Actions and Modules.
|
||||||
|
|
||||||
|
When a user clicks on an action, we need to `dispatch` it. This action will `commit` a mutation that will change the state.
|
||||||
|
_Note:_ The action itself will not update the state, only a mutation should update the state.
|
||||||
|
|
||||||
|
## File structure
|
||||||
|
When using Vuex at GitLab, separate this concerns into different files to improve readability:
|
||||||
|
|
||||||
|
```
|
||||||
|
└── store
|
||||||
|
├── index.js # where we assemble modules and export the store
|
||||||
|
├── actions.js # actions
|
||||||
|
├── mutations.js # mutations
|
||||||
|
├── getters.js # getters
|
||||||
|
├── state.js # state
|
||||||
|
└── mutation_types.js # mutation types
|
||||||
|
```
|
||||||
|
The following example shows an application that lists and adds users to the state.
|
||||||
|
(For a more complex example implementation take a look at the security applications store in [here](https://gitlab.com/gitlab-org/gitlab-ee/tree/master/ee/app/assets/javascripts/vue_shared/security_reports/store))
|
||||||
|
|
||||||
|
### `index.js`
|
||||||
|
This is the entry point for our store. You can use the following as a guide:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Vuex from 'vuex';
|
||||||
|
import * as actions from './actions';
|
||||||
|
import * as getters from './getters';
|
||||||
|
import mutations from './mutations';
|
||||||
|
import state from './state';
|
||||||
|
|
||||||
|
Vue.use(Vuex);
|
||||||
|
|
||||||
|
export default new Vuex.Store({
|
||||||
|
actions,
|
||||||
|
getters,
|
||||||
|
mutations,
|
||||||
|
state,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### `state.js`
|
||||||
|
The first thing you should do before writing any code is to design the state.
|
||||||
|
|
||||||
|
Often we need to provide data from haml to our Vue application. Let's store it in the state for better access.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export default {
|
||||||
|
endpoint: null,
|
||||||
|
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
|
||||||
|
isAddingUser: false,
|
||||||
|
errorAddingUser: false,
|
||||||
|
|
||||||
|
users: [],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Access `state` properties
|
||||||
|
You can use `mapState` to access state properties in the components.
|
||||||
|
|
||||||
|
### `actions.js`
|
||||||
|
An action is a playload of information to send data from our application to our store.
|
||||||
|
|
||||||
|
An action is usually composed by a `type` and a `payload` and they describe what happened.
|
||||||
|
Enforcing that every change is described as an action lets us have a clear understanting of what is going on in the app.
|
||||||
|
|
||||||
|
In this file, we will write the actions that will call the respective mutations:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import * as types from './mutation_types';
|
||||||
|
import axios from '~/lib/utils/axios-utils';
|
||||||
|
import createFlash from '~/flash';
|
||||||
|
|
||||||
|
export const requestUsers = ({ commit }) => commit(types.REQUEST_USERS);
|
||||||
|
export const receiveUsersSuccess = ({ commit }, data) => commit(types.RECEIVE_USERS_SUCCESS, data);
|
||||||
|
export const receiveUsersError = ({ commit }, error) => commit(types.REQUEST_USERS_ERROR, error);
|
||||||
|
|
||||||
|
export const fetchUsers = ({ state, dispatch }) => {
|
||||||
|
dispatch('requestUsers');
|
||||||
|
|
||||||
|
axios.get(state.endoint)
|
||||||
|
.then(({ data }) => dispatch('receiveUsersSuccess', data))
|
||||||
|
.catch((error) => {
|
||||||
|
dispatch('receiveUsersError', error)
|
||||||
|
createFlash('There was an error')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const requestAddUser = ({ commit }) => commit(types.REQUEST_ADD_USER);
|
||||||
|
export const receiveAddUserSuccess = ({ commit }, data) => commit(types.RECEIVE_ADD_USER_SUCCESS, data);
|
||||||
|
export const receiveAddUserError = ({ commit }, error) => commit(types.REQUEST_ADD_USER_ERROR, error);
|
||||||
|
|
||||||
|
export const addUser = ({ state, dispatch }, user) => {
|
||||||
|
dispatch('requestAddUser');
|
||||||
|
|
||||||
|
axios.post(state.endoint, user)
|
||||||
|
.then(({ data }) => dispatch('receiveAddUserSuccess', data))
|
||||||
|
.catch((error) => dispatch('receiveAddUserError', error));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Actions Pattern: `request` and `receive` namespaces
|
||||||
|
When a request is made we often want to show a loading state to the user.
|
||||||
|
|
||||||
|
Instead of creating an action to toggle the loading state and dispatch it in the component,
|
||||||
|
create:
|
||||||
|
1. An action `requestSomething`, to toggle the loading state
|
||||||
|
1. An action `receiveSomethingSuccess`, to handle the success callback
|
||||||
|
1. An action `receiveSomethingError`, to handle the error callback
|
||||||
|
1. An action `fetchSomething` to make the request.
|
||||||
|
1. In case your application does more than a `GET` request you can use these as examples:
|
||||||
|
1. `PUT`: `createSomething`
|
||||||
|
2. `POST`: `updateSomething`
|
||||||
|
3. `DELETE`: `deleteSomething`
|
||||||
|
|
||||||
|
The component MUST only dispatch the `fetchNamespace` action. Actions namespaced with `request` or `receive` should not be called from the component
|
||||||
|
The `fetch` action will be responsible to dispatch `requestNamespace`, `receiveNamespaceSuccess` and `receiveNamespaceError`
|
||||||
|
|
||||||
|
By following this pattern we guarantee:
|
||||||
|
1. All aplications follow the same pattern, making it easier for anyone to maintain the code
|
||||||
|
1. All data in the application follows the same lifecycle pattern
|
||||||
|
1. Actions are contained and human friendly
|
||||||
|
1. Unit tests are easier
|
||||||
|
1. Actions are simple and straightforward
|
||||||
|
|
||||||
|
#### Dispatching actions
|
||||||
|
To dispatch an action from a component, use the `mapActions` helper:
|
||||||
|
```javascript
|
||||||
|
import { mapActions } from 'vuex';
|
||||||
|
|
||||||
|
{
|
||||||
|
methods: {
|
||||||
|
...mapActions([
|
||||||
|
'addUser',
|
||||||
|
]),
|
||||||
|
onClickUser(user) {
|
||||||
|
this.addUser(user);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `mutations.js`
|
||||||
|
The mutations specify how the application state changes in response to actions sent to the store.
|
||||||
|
The only way to change state in a Vuex store should be by committing a mutation.
|
||||||
|
|
||||||
|
**It's a good idea to think of the state before writing any code.**
|
||||||
|
|
||||||
|
Remember that actions only describe that something happened, they don't describe how the application state changes.
|
||||||
|
|
||||||
|
**Never commit a mutation directly from a component**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import * as types from './mutation_types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
[types.REQUEST_USERS](state) {
|
||||||
|
state.isLoading = true;
|
||||||
|
},
|
||||||
|
[types.RECEIVE_USERS_SUCCESS](state, data) {
|
||||||
|
// Do any needed data transformation to the received payload here
|
||||||
|
state.users = data;
|
||||||
|
state.isLoading = false;
|
||||||
|
},
|
||||||
|
[types.REQUEST_USERS_ERROR](state, error) {
|
||||||
|
state.isLoading = false;
|
||||||
|
},
|
||||||
|
[types.REQUEST_ADD_USER](state, user) {
|
||||||
|
state.isAddingUser = true;
|
||||||
|
},
|
||||||
|
[types.RECEIVE_ADD_USER_SUCCESS](state, user) {
|
||||||
|
state.isAddingUser = false;
|
||||||
|
state.users.push(user);
|
||||||
|
},
|
||||||
|
[types.REQUEST_ADD_USER_ERROR](state, error) {
|
||||||
|
state.isAddingUser = true;
|
||||||
|
state.errorAddingUser = error∂;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `getters.js`
|
||||||
|
Sometimes we may need to get derived state based on store state, like filtering for a specific prop.
|
||||||
|
Using a getter will also cache the result based on dependencies due to [how computed props work](https://vuejs.org/v2/guide/computed.html#Computed-Caching-vs-Methods)
|
||||||
|
This can be done through the `getters`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// get all the users with pets
|
||||||
|
export const getUsersWithPets = (state, getters) => {
|
||||||
|
return state.users.filter(user => user.pet !== undefined);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
To access a getter from a component, use the `mapGetters` helper:
|
||||||
|
```javascript
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
{
|
||||||
|
computed: {
|
||||||
|
...mapGetters([
|
||||||
|
'getUsersWithPets',
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `mutations_types.js`
|
||||||
|
From [vuex mutations docs][vuex-mutations]:
|
||||||
|
> It is a commonly seen pattern to use constants for mutation types in various Flux implementations. This allows the code to take advantage of tooling like linters, and putting all constants in a single file allows your collaborators to get an at-a-glance view of what mutations are possible in the entire application.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export const ADD_USER = 'ADD_USER';
|
||||||
|
```
|
||||||
|
|
||||||
|
### How to include the store in your application
|
||||||
|
The store should be included in the main component of your application:
|
||||||
|
```javascript
|
||||||
|
// app.vue
|
||||||
|
import store from 'store'; // it will include the index.js file
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'application',
|
||||||
|
store,
|
||||||
|
...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Communicating with the Store
|
||||||
|
```javascript
|
||||||
|
<script>
|
||||||
|
import { mapActions, mapState, mapGetters } from 'vuex';
|
||||||
|
import store from './store';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
store,
|
||||||
|
computed: {
|
||||||
|
...mapGetters([
|
||||||
|
'getUsersWithPets'
|
||||||
|
]),
|
||||||
|
...mapState([
|
||||||
|
'isLoading',
|
||||||
|
'users',
|
||||||
|
'error',
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions([
|
||||||
|
'fetchUsers',
|
||||||
|
'addUser',
|
||||||
|
]),
|
||||||
|
|
||||||
|
onClickAddUser(data) {
|
||||||
|
this.addUser(data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.fetchUsers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<ul>
|
||||||
|
<li v-if="isLoading">
|
||||||
|
Loading...
|
||||||
|
</li>
|
||||||
|
<li v-else-if="error">
|
||||||
|
{{ error }}
|
||||||
|
</li>
|
||||||
|
<template v-else>
|
||||||
|
<li
|
||||||
|
v-for="user in users"
|
||||||
|
:key="user.id"
|
||||||
|
>
|
||||||
|
{{ user }}
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vuex Gotchas
|
||||||
|
1. Do not call a mutation directly. Always use an action to commit a mutation. Doing so will keep consistency through out the application. From Vuex docs:
|
||||||
|
|
||||||
|
> why don't we just call store.commit('action') directly? Well, remember that mutations must be synchronous? Actions aren't. We can perform asynchronous operations inside an action.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// component.vue
|
||||||
|
|
||||||
|
// bad
|
||||||
|
created() {
|
||||||
|
this.$store.commit('mutation');
|
||||||
|
}
|
||||||
|
|
||||||
|
// good
|
||||||
|
created() {
|
||||||
|
this.$store.dispatch('action');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
1. Use mutation types instead of hardcoding strings. It will be less error prone.
|
||||||
|
1. The State will be accessible in all components descending from the use where the store is instantiated.
|
||||||
|
|
||||||
|
### Testing Vuex
|
||||||
|
#### Testing Vuex concerns
|
||||||
|
Refer to [vuex docs][vuex-testing] regarding testing Actions, Getters and Mutations.
|
||||||
|
|
||||||
|
#### Testing components that need a store
|
||||||
|
Smaller components might use `store` properties to access the data.
|
||||||
|
In order to write unit tests for those components, we need to include the store and provide the correct state:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
//component_spec.js
|
||||||
|
import Vue from 'vue';
|
||||||
|
import store from './store';
|
||||||
|
import component from './component.vue'
|
||||||
|
|
||||||
|
describe('component', () => {
|
||||||
|
let vm;
|
||||||
|
let Component;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
Component = Vue.extend(issueActions);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vm.$destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a user', () => {
|
||||||
|
const user = {
|
||||||
|
name: 'Foo',
|
||||||
|
age: '30',
|
||||||
|
};
|
||||||
|
|
||||||
|
// populate the store
|
||||||
|
store.dipatch('addUser', user);
|
||||||
|
|
||||||
|
vm = new Component({
|
||||||
|
store,
|
||||||
|
propsData: props,
|
||||||
|
}).$mount();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
[vuex-docs]: https://vuex.vuejs.org
|
||||||
|
[vuex-structure]: https://vuex.vuejs.org/en/structure.html
|
||||||
|
[vuex-mutations]: https://vuex.vuejs.org/en/mutations.html
|
||||||
|
[vuex-testing]: https://vuex.vuejs.org/en/testing.html
|
Loading…
Reference in a new issue