Improve state management documentation
This commit is contained in:
parent
9027d023a5
commit
fef1e9d47b
|
@ -1,29 +1,7 @@
|
|||
# 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].
|
||||
|
||||
## 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
|
||||
|
||||
All new features built with Vue.js must follow a [Flux architecture][flux].
|
||||
|
@ -57,15 +35,15 @@ new_feature
|
|||
│ └── ...
|
||||
├── stores
|
||||
│ └── new_feature_store.js
|
||||
├── services
|
||||
├── services # only when not using vuex
|
||||
│ └── new_feature_service.js
|
||||
├── new_feature_bundle.js
|
||||
├── index.js
|
||||
```
|
||||
_For consistency purposes, we recommend you to follow the same structure._
|
||||
|
||||
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
|
||||
of the new feature should be.
|
||||
|
@ -144,30 +122,15 @@ 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]
|
||||
|
||||
#### 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.
|
||||
A `prop` and a property returned by the `data` functions require `vue` to set a `getter` and a `setter` for each of them.
|
||||
The SVG should be a computed property in order to improve performance, note that computed properties are cached based on their dependencies.
|
||||
|
||||
```javascript
|
||||
// bad
|
||||
import svg from 'svg.svg';
|
||||
data() {
|
||||
return {
|
||||
myIcon: svg,
|
||||
};
|
||||
};
|
||||
|
||||
// good
|
||||
import svg from 'svg.svg';
|
||||
computed: {
|
||||
myIcon() {
|
||||
return svg;
|
||||
}
|
||||
}
|
||||
```
|
||||
1. Using SVGs icons in components: To use an SVG icon in a template use the `icon.vue`
|
||||
1. Using SVGs illustrations in components: To use an SVG illustrations in a template provide the path as a prop.
|
||||
|
||||
### 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
|
||||
source of truth. It is not aware of the service or the components.
|
||||
|
||||
|
@ -176,6 +139,8 @@ itself, please read this guide: [State Management][state-management]
|
|||
|
||||
### 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.
|
||||
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.
|
||||
|
@ -273,6 +238,9 @@ import Store from 'store';
|
|||
import Service from 'service';
|
||||
import TodoComponent from 'todoComponent';
|
||||
export default {
|
||||
components: {
|
||||
todo: TodoComponent,
|
||||
},
|
||||
/**
|
||||
* 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
|
||||
|
@ -291,10 +259,6 @@ export default {
|
|||
};
|
||||
},
|
||||
|
||||
components: {
|
||||
todo: TodoComponent,
|
||||
},
|
||||
|
||||
created() {
|
||||
this.service = new Service('todos');
|
||||
|
||||
|
@ -476,200 +440,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)
|
||||
|
||||
|
||||
## 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 mutatation. In this file, we will write the actions that will call 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.dipatch('addUser', user);
|
||||
|
||||
vm = new Component({
|
||||
store,
|
||||
propsData: props,
|
||||
}).$mount();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
[vue-docs]: http://vuejs.org/guide/index.html
|
||||
[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards
|
||||
|
@ -681,9 +451,5 @@ describe('component', () => {
|
|||
[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
|
||||
[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-interceptors]: https://github.com/axios/axios#interceptors
|
||||
|
|
|
@ -0,0 +1,261 @@
|
|||
|
||||
## 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 is a playload of information to send data from our application to our store.
|
||||
They are the only source of information for the store.
|
||||
|
||||
An action is usually composed by a `type` and a `payload` and they describe what happened.
|
||||
By enforcing that every change is described as an action lets us have a clear understantid of what is going on in the app.
|
||||
|
||||
An action represents something that will trigger a state change, for example, when the user enters the page we need to load resources.
|
||||
|
||||
In this file, we will write the actions (both sync and async) that will call the respective mutations:
|
||||
|
||||
```javascript
|
||||
import * as types from './mutation_types';
|
||||
import axios from '~/lib/utils/axios-utils';
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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. A sync action `requestSomething`, to toggle the loading state
|
||||
1. A sync action `receiveSomethingSuccess`, to handle the success callback
|
||||
1. A sync action `receiveSomethingError`, to handle the error callback
|
||||
1. An async action `fetchSomething` to make the request.
|
||||
|
||||
The component MUST only dispatch the `fetchNamespace` action.
|
||||
The `fetch` action will be responsible to dispatch `requestNamespace`, `receiveNamespaceSuccess` and `receiveNamespaceError`
|
||||
|
||||
By following this patter 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 actually change state in a Vuex store is by committing a mutation.
|
||||
|
||||
**It's a good idea to think of the state shape before writing any code.**
|
||||
|
||||
Remember that actions only describe the fact 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.ADD_USER](state, user) {
|
||||
state.users.push(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_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. 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 New Issue