vuex.md 10.9 KB
Newer Older
1
# Vuex
2

3
To manage the state of an application you should use [Vuex][vuex-docs].
4 5 6

_Note:_ All of the below is explained in more detail in the official [Vuex documentation][vuex-docs].

7
## Separation of concerns
8

9 10 11 12 13
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.

14
## File structure
15

16
When using Vuex at GitLab, separate these concerns into different files to improve readability:
17 18 19 20 21 22 23

```
└── store
  ├── index.js          # where we assemble modules and export the store
  ├── actions.js        # actions
  ├── mutations.js      # mutations
  ├── getters.js        # getters
Filipa Lacerda's avatar
Filipa Lacerda committed
24
  ├── state.js          # state
25 26
  └── mutation_types.js # mutation types
```
27

28
The following example shows an application that lists and adds users to the state.
29
(For a more complex example implementation take a look at the security applications store in [here](https://gitlab.com/gitlab-org/gitlab/tree/master/ee/app/assets/javascripts/vue_shared/security_reports/store))
30

31
### `index.js`
32

33 34 35 36 37 38 39 40
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';
41
import state from './state';
42 43 44

Vue.use(Vuex);

45
export const createStore = () => new Vuex.Store({
46 47 48
  actions,
  getters,
  mutations,
49
  state,
50
});
51
export default createStore();
52 53
```

54
### `state.js`
55

56 57 58 59 60
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
61
  export default () => ({
62 63 64 65 66 67 68 69 70
    endpoint: null,

    isLoading: false,
    error: null,

    isAddingUser: false,
    errorAddingUser: false,

    users: [],
71
  });
72 73 74
```

#### Access `state` properties
75

76 77 78
You can use `mapState` to access state properties in the components.

### `actions.js`
79

Dennis Tang's avatar
Dennis Tang committed
80
An action is a payload of information to send data from our application to our store.
81 82

An action is usually composed by a `type` and a `payload` and they describe what happened.
Dennis Tang's avatar
Dennis Tang committed
83
Enforcing that every change is described as an action lets us have a clear understanding of what is going on in the app.
84

Filipa Lacerda's avatar
Filipa Lacerda committed
85
In this file, we will write the actions that will call the respective mutations:
86 87 88

```javascript
  import * as types from './mutation_types';
Eric Eastwood's avatar
Eric Eastwood committed
89
  import axios from '~/lib/utils/axios_utils';
Filipa Lacerda's avatar
Filipa Lacerda committed
90
  import createFlash from '~/flash';
91 92 93

  export const requestUsers = ({ commit }) => commit(types.REQUEST_USERS);
  export const receiveUsersSuccess = ({ commit }, data) => commit(types.RECEIVE_USERS_SUCCESS, data);
94
  export const receiveUsersError = ({ commit }, error) => commit(types.RECEIVE_USERS_ERROR, error);
95 96 97 98

  export const fetchUsers = ({ state, dispatch }) => {
    dispatch('requestUsers');

Dennis Tang's avatar
Dennis Tang committed
99
    axios.get(state.endpoint)
100
      .then(({ data }) => dispatch('receiveUsersSuccess', data))
Filipa Lacerda's avatar
Filipa Lacerda committed
101 102 103 104
      .catch((error) => {
        dispatch('receiveUsersError', error)
        createFlash('There was an error')
      });
105 106 107 108 109 110 111 112 113
  }

  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');

Dennis Tang's avatar
Dennis Tang committed
114
    axios.post(state.endpoint, user)
115 116 117 118 119
      .then(({ data }) => dispatch('receiveAddUserSuccess', data))
      .catch((error) => dispatch('receiveAddUserError', error));
  }
```

120
#### Actions Pattern: `request` and `receive` namespaces
121

122 123 124 125
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:
126

Filipa Lacerda's avatar
Filipa Lacerda committed
127 128 129
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
130 131
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:
132 133
        - `POST`: `createSomething`
        - `PUT`: `updateSomething`
134
        - `DELETE`: `deleteSomething`
135

Filipa Lacerda's avatar
Filipa Lacerda committed
136
The component MUST only dispatch the `fetchNamespace` action. Actions namespaced with `request` or `receive` should not be called from the component
137 138
The `fetch` action will be responsible to dispatch `requestNamespace`, `receiveNamespaceSuccess` and `receiveNamespaceError`

Filipa Lacerda's avatar
Filipa Lacerda committed
139
By following this pattern we guarantee:
140

Dennis Tang's avatar
Dennis Tang committed
141
1. All applications follow the same pattern, making it easier for anyone to maintain the code
142 143 144 145 146
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

147
#### Dispatching actions
148

149
To dispatch an action from a component, use the `mapActions` helper:
150

151 152 153 154 155 156 157 158 159 160 161 162 163 164 165
```javascript
import { mapActions } from 'vuex';

{
  methods: {
    ...mapActions([
      'addUser',
    ]),
    onClickUser(user) {
      this.addUser(user);
    },
  },
};
```

166
### `mutations.js`
167

168
The mutations specify how the application state changes in response to actions sent to the store.
Filipa Lacerda's avatar
Filipa Lacerda committed
169
The only way to change state in a Vuex store should be by committing a mutation.
170

Filipa Lacerda's avatar
Filipa Lacerda committed
171
**It's a good idea to think of the state before writing any code.**
172

Filipa Lacerda's avatar
Filipa Lacerda committed
173
Remember that actions only describe that something happened, they don't describe how the application state changes.
174 175 176 177 178 179 180

**Never commit a mutation directly from a component**

```javascript
  import * as types from './mutation_types';

  export default {
181
    [types.REQUEST_USERS](state) {
Filipa Lacerda's avatar
Filipa Lacerda committed
182
      state.isLoading = true;
183 184 185
    },
    [types.RECEIVE_USERS_SUCCESS](state, data) {
      // Do any needed data transformation to the received payload here
Filipa Lacerda's avatar
Filipa Lacerda committed
186 187
      state.users = data;
      state.isLoading = false;
188
    },
189
    [types.RECEIVE_USERS_ERROR](state, error) {
Filipa Lacerda's avatar
Filipa Lacerda committed
190
      state.isLoading = false;
191 192
    },
    [types.REQUEST_ADD_USER](state, user) {
Dennis Tang's avatar
Dennis Tang committed
193
      state.isAddingUser = true;
194 195
    },
    [types.RECEIVE_ADD_USER_SUCCESS](state, user) {
Filipa Lacerda's avatar
Filipa Lacerda committed
196
      state.isAddingUser = false;
197 198
      state.users.push(user);
    },
199
    [types.REQUEST_ADD_USER_ERROR](state, error) {
200
      state.isAddingUser = false;
Dennis Tang's avatar
Dennis Tang committed
201
      state.errorAddingUser = error;
202
    },
203 204 205
  };
```

206
### `getters.js`
207

208
Sometimes we may need to get derived state based on store state, like filtering for a specific prop.
Filipa Lacerda's avatar
Filipa Lacerda committed
209
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)
210 211 212 213
This can be done through the `getters`:

```javascript
// get all the users with pets
Filipa Lacerda's avatar
Filipa Lacerda committed
214
export const getUsersWithPets = (state, getters) => {
215 216 217 218 219
  return state.users.filter(user => user.pet !== undefined);
};
```

To access a getter from a component, use the `mapGetters` helper:
220

221 222 223 224 225 226 227 228 229 230 231 232
```javascript
import { mapGetters } from 'vuex';

{
  computed: {
    ...mapGetters([
      'getUsersWithPets',
    ]),
  },
};
```

Eric Eastwood's avatar
Eric Eastwood committed
233
### `mutation_types.js`
234

235
From [vuex mutations docs](https://vuex.vuejs.org/guide/mutations.html):
236 237 238 239 240 241 242
> 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
243

244
The store should be included in the main component of your application:
245

246 247
```javascript
  // app.vue
248
  import store from './store'; // it will include the index.js file
249 250 251 252 253 254 255 256

  export default {
    name: 'application',
    store,
    ...
  };
```

257
### Communicating with the Store
258

259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299
```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>
Filipa Lacerda's avatar
Filipa Lacerda committed
300 301 302 303 304 305 306 307
    <template v-else>
      <li
        v-for="user in users"
        :key="user.id"
      >
        {{ user }}
      </li>
    </template>
308 309 310 311
  </ul>
</template>
```

312 313
### Vuex Gotchas

314
1. Do not call a mutation directly. Always use an action to commit a mutation. Doing so will keep consistency throughout the application. From Vuex docs:
315

316
   > 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.
317

318 319
   ```javascript
     // component.vue
320

321 322 323 324 325 326 327 328 329 330
     // bad
     created() {
       this.$store.commit('mutation');
     }

     // good
     created() {
       this.$store.dispatch('action');
     }
   ```
331

332 333 334 335
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
336

337
#### Testing Vuex concerns
338

339
Refer to [vuex docs](https://vuex.vuejs.org/guide/testing.html) regarding testing Actions, Getters and Mutations.
340 341

#### Testing components that need a store
342

343 344 345 346 347 348
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';
349
import { createStore } from './store';
350 351 352
import component from './component.vue'

describe('component', () => {
353
  let store;
354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369
  let vm;
  let Component;

  beforeEach(() => {
    Component = Vue.extend(issueActions);
  });

  afterEach(() => {
    vm.$destroy();
  });

  it('should show a user', () => {
    const user = {
      name: 'Foo',
      age: '30',
    };
370

371
    store = createStore();
372 373

    // populate the store
Dennis Tang's avatar
Dennis Tang committed
374
    store.dispatch('addUser', user);
375 376 377 378 379 380 381 382 383

    vm = new Component({
      store,
      propsData: props,
    }).$mount();
  });
});
```

384
#### Testing Vuex actions and getters
385

386
Because we're currently using [`babel-plugin-rewire`](https://github.com/speedskater/babel-plugin-rewire), you may encounter the following error when testing your Vuex actions and getters:
387 388
`[vuex] actions should be function or object with "handler" function`

389
To prevent this error from happening, you need to export an empty function as `default`:
390 391

```javascript
Dennis Tang's avatar
Dennis Tang committed
392
// getters.js or actions.js
393 394 395 396 397

// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
```

398
[vuex-docs]: https://vuex.vuejs.org