Introducing Vuex in your projects How to add and use Vuex in existing projects, with an eye for testing.   - Denny Biasiolli -
WHO AM I Denny Biasiolli Freelance Full Stack Developer Front End Developer UX/ UI Fingerprint Supervision Ltd Savigliano (CN) - Italy Volunteer in a retirement home, performing recreational activities @dennybiasiolli denny.biasiolli@gmail.com dennybiasiolli.com
EXAMPLE APP
EXAMPLE APP Structure
EXAMPLE APP Main component, data() export default { name: 'Home', data() { // component's state return { availableNumbers: [...Array(90).keys()] .map((i) => i + 1), extractedNumbers: [], }; }, // ... };
EXAMPLE APP Main component, computed export default { // ... computed: { // component's getters ascendingExtractedNumbers() { return [...this.extractedNumbers].sort((a, b) => a - b); }, }, // ... };
EXAMPLE APP Main component, methods export default { // ... methods: { // component's actions/mutations handleExtract() { const index = Math.floor( Math.random() * this.availableNumbers.length); const extracted = this.availableNumbers .splice(index, 1); this.extractedNumbers = this.extractedNumbers .concat(extracted); }, }, };
EXAMPLE APP Main component template <button @click="handleExtract">Extract</button> <h1> Extracted: {{ extractedNumbers[extractedNumbers.length - 1] }} </h1> <DisplayNumbers title="Available numbers" :numbers="availableNumbers" /> <DisplayNumbers title="Extracted numbers" :numbers="ascendingExtractedNumbers" />
EXAMPLE APP DisplayNumbers component <v-card elevation="2"> <v-card-title>{{ title }}</v-card-title> <v-card-text> <v-chip v-for="n of numbers" :key="n" class="ma-1"> {{ n }} </v-chip> </v-card-text> </v-card> export default { name: 'DisplayNumbers', props: { title: String, numbers: Array, }, };
COMPONENT TESTS DisplayNumbers import { shallowMount } from '@vue/test-utils'; import DisplayNumbers from '@/components/DisplayNumbers.vue'; test('renders as expected', () => { const wrapper = shallowMount(DisplayNumbers, { stubs: ['v-container', 'v-card', 'v-card-title', 'v-card-t propsData: { title: 'title text', numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], }, }); expect(wrapper).toMatchSnapshot(); });
COMPONENT TESTS Home #1 import { shallowMount } from '@vue/test-utils'; import Home from '@/views/Home.vue'; const shallowMountComponent = () => shallowMount(Home, { stubs: ['v-container', 'v-btn', 'v-row', 'v-col'], }); test('renders as expected', () => { const wrapper = shallowMountComponent(); expect(wrapper).toMatchSnapshot(); }); // ...
COMPONENT TESTS Home #2 // ... test('extracts a number and render as expected', async () => { jest.spyOn(global.Math, 'random') .mockReturnValueOnce(0.123456789) .mockReturnValueOnce(0.987654321); const wrapper = shallowMountComponent(); wrapper.vm.handleExtract(); await wrapper.vm.$nextTick(); expect(wrapper).toMatchSnapshot(); wrapper.vm.handleExtract(); await wrapper.vm.$nextTick(); expect(wrapper).toMatchSnapshot(); jest.spyOn(global.Math, 'random').mockRestore(); });
ONE-WAY DATA FLOW
STATE FLOW SUMMARY Flow process Vue.js component State data and computed View <template> Actions methods
STATE PROBLEM
Solution 1: Moving state to parent components move data() from Home to App receiving numbers in Home and Footer as props emitting an event when "Extract" button is clicked in Home handling extract event in App component, moving methods from Home to App updating tests
PROS fast and easy in small apps keep the state in the components where it is used (if there is no need to pass it to other components) no extra dependencies testing sub-components with propsData and snapshots
CONS multiple views may depend on the same piece of state actions from different views may need to mutate the same piece of state messy on big apps, lots of extra code for passing props, emitting events hard to follow state changes on many levels what is causing a data change?
WHAT IS VUEX? A state management pattern/library for Vue.js applications. It serves as a centralized store for all the components in an application, with rules ensuring that the state can only be mutated in a predictable fashion. https://vuex.vuejs.org/
WHEN SHOULD I USE IT? There's a good quote from Dan Abramov, the author of Redux: Flux libraries are like glasses: you’ll know when you need them. https://vuex.vuejs.org/
WHEN SHOULD I USE IT? It's a trade-off between short term and long term productivity. If you jump right into Vuex, it may feel verbose and daunting. But if you are building a medium-to-large-scale SPA, chances are you have run into situations that make you think about how to better handle state outside of your Vue components, and Vuex will be the natural next step for you. https://vuex.vuejs.org/
VUEX FLOW
INSTALL VUEX or <script src="/path/to/vue.js"></script> <script src="/path/to/vuex.js"></script> npm install --save vuex # or yarn add vuex # https://yarnpkg.com/ # or npx @vue/cli add vuex # https://cli.vuejs.org/ import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); https://vuex.vuejs.org/installation.html
CONFIGURE VUEX Creating the store // src/store/index.js import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); export default new Vuex.Store({ state: { /* ... */ }, mutations: { /* ... */ }, }); https://vuex.vuejs.org/guide/
CONFIGURE VUEX Enabling this.$store inside Vue components // src/main.js // ... import store from './store'; new Vue({ store, // same as `store: store` // ... }); https://vuex.vuejs.org/guide/
CONCEPTS: STATE Creation new Vuex.Store({ state: { count: 0 }, // ... }); https://vuex.vuejs.org/guide/state.html
CONCEPTS: STATE Basic usage <div> {{ $store.state.count }} {{ count }} </div> computed: { count () { return this.$store.state.count; } } https://vuex.vuejs.org/guide/state.html
CONCEPTS: STATE mapState usage import { mapState } from 'vuex'; export default { // ... computed: mapState({ count: state => state.count, countAlias: 'count', // to access local state with `this` countPlusLocalState (state) { return state.count + this.localCount; } }) }; https://vuex.vuejs.org/guide/state.html
CONCEPTS: STATE mapState usage simplified is the same as mapState({ count: state => state.count }) mapState([ 'count' ]) https://vuex.vuejs.org/guide/state.html
CONCEPTS: STATE mapState usage with other computed values computed: { ...mapState({ // ... }), localComputed () { /* ... */ } } https://vuex.vuejs.org/guide/state.html
CONCEPTS: GETTERS Getters are like "computed" values for a Vuex store Creation const store = new Vuex.Store({ state: { count: 0 }, getters: { countIsEven: state => { return state.count % 2 === 0; } } }); https://vuex.vuejs.org/guide/getters.html
CONCEPTS: GETTERS Basic usage <div> {{ $store.getters.countIsEven }} {{ countIsEven }} </div> computed: { countIsEven () { return this.$store.getters.countIsEven; } } https://vuex.vuejs.org/guide/getters.html
CONCEPTS: GETTERS mapGetters usage import { mapGetters } from 'vuex'; export default { // ... computed: mapGetters({ countIsEvenAlias: 'countIsEven' }) }; https://vuex.vuejs.org/guide/getters.html
CONCEPTS: GETTERS mapGetters advanced usage computed: { ...mapState(['count']), ...mapGetters(['countIsEven']), localComputed () { /* ... */ } } https://vuex.vuejs.org/guide/getters.html
CONCEPTS: MUTATIONS Committing a mutation is the only way to actually change state in a Vuex store. Creation const store = new Vuex.Store({ state: { count: 0 }, mutations: { increment (state, payload=1) { state.count += payload; } } }); https://vuex.vuejs.org/guide/mutations.html
CONCEPTS: MUTATIONS Basic usage methods: { increment (value) { return this.$store.commit('increment', value); } } https://vuex.vuejs.org/guide/mutations.html
CONCEPTS: MUTATIONS mapMutations usage import { mapMutations } from 'vuex'; export default { // ... methods: { ...mapMutations([ 'increment' ]), ...mapMutations({ add: 'increment' }) } }; https://vuex.vuejs.org/guide/mutations.html
MUTATIONS MUST BE SYNCHRONOUS Why? Because we need to have a "before" and "a er" snapshots of the state. If we introduce a callback inside a mutation, it makes that impossible. The callback is not called yet when the mutation is committed, and there's no way to know when the callback will actually be called. Any state mutation performed in the callback is essentially un-trackable! https://vuex.vuejs.org/guide/mutations.html
CONCEPTS: ACTIONS Actions are similar to mutations, with a few differences: Instead of mutating the state, actions commit mutations. Actions can contain arbitrary asynchronous operations. https://vuex.vuejs.org/guide/actions.html
CONCEPTS: ACTIONS Creation const store = new Vuex.Store({ state: { count: 0 }, mutations: { increment (state, payload=1) { state.count += payload; } }, actions: { incrementAsync (context, payload) { setTimeout(() => { context.commit('increment', payload); }, 1000); } } }); https://vuex.vuejs.org/guide/actions.html
CONCEPTS: ACTIONS API call example const store = new Vuex.Store({ actions: { async getRecords (context) { context.commit('getRecordsRequest'); try { const results = await axios.get('/api/records/'); context.commit('getRecordsSuccess', results.data); } catch (error) { context.commit('getRecordsFailure', error); } } } }); https://vuex.vuejs.org/guide/actions.html
CONCEPTS: ACTIONS Context object context.commit to commit a mutation context.state access the state context.getters access the getters context.dispatch to call other actions https://vuex.vuejs.org/guide/actions.html
CONCEPTS: ACTIONS mapActions usage import { mapActions } from 'vuex'; export default { // ... methods: { incrementAsyncLocal (value) { return this.$store.dispatch('incrementAsync', value) .then( /* ... */); } ...mapActions(['incrementAsync']), ...mapActions({ addAsync: 'incrementAsync' }) } }; https://vuex.vuejs.org/guide/actions.html
EXAMPLE APP Creating the store, default state // src/store/index.js import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); export const defaultState = { availableNumbers: [...Array(90).keys()] .map((i) => i + 1), extractedNumbers: [], };
EXAMPLE APP Creating the store, getters // src/store/index.js export const getters = { ascendingExtractedNumbers(state) { return [...state.extractedNumbers].sort((a, b) => a - b); }, };
EXAMPLE APP Creating the store, mutations // src/store/index.js export const mutations = { extractNumber(state) { const index = Math.floor( Math.random() * state.availableNumbers.length); const extracted = state.availableNumbers .splice(index, 1); state.extractedNumbers = state.extractedNumbers .concat(extracted); }, };
EXAMPLE APP Creating the store, composing // src/store/index.js export default new Vuex.Store({ state: defaultState, getters, mutations, });
EXAMPLE APP Home component, data and computed function @@ src/views/Home.vue - data() { - return { - availableNumbers: [...Array(90).keys()].map((i) => i + - extractedNumbers: [], - }; - }, computed: { - ascendingExtractedNumbers() { - return [...this.extractedNumbers].sort((a, b) => a - b) - }, + ...mapState(['availableNumbers', 'extractedNumbers']), + ...mapGetters(['ascendingExtractedNumbers']), },
EXAMPLE APP Home component, method @@ src/views/Home.vue - <button @click="handleExtract">Extract</button> + <button @click="extractNumber">Extract</button> methods: { - handleExtract() { - const index = Math.floor(Math.random() * this.available - const extracted = this.availableNumbers.splice(index, 1 - this.extractedNumbers = this.extractedNumbers.concat(ex - }, + ...mapMutations(['extractNumber']), },
MOVING TO VUEX STORE FLOW Vue.js component Vuex store Map in data state computed computed getters computed sync methods mutations methods async methods actions methods
STORE TESTS Default state import { defaultState } from '@/store'; test('should have the default state', () => { expect(defaultState).toEqual({ availableNumbers: [...Array(90).keys()].map((i) => i + 1), extractedNumbers: [], }); });
STORE TESTS Getters import { getters } from '@/store'; const { ascendingExtractedNumbers } = getters; test('ascendingExtractedNumbers', () => { expect( ascendingExtractedNumbers({ extractedNumbers: [12, 56, 34] }) ).toEqual([12, 34, 56]); });
STORE TESTS Mutations import { mutations } from '@/store'; const { defaultState, extractNumber } = mutations; test('ascendingExtractedNumbers', () => { jest.spyOn(global.Math, 'random') .mockReturnValueOnce(0.123456789) .mockReturnValueOnce(0.987654321); const state = { ...defaultState }; expect(state.availableNumbers).toHaveLength(90); // ...
STORE TESTS Mutations // ... extractNumber(state); expect(state.availableNumbers).toHaveLength(89); expect(state.extractedNumbers).toEqual([12]); extractNumber(state); expect(state.availableNumbers).toHaveLength(88); expect(state.extractedNumbers).toEqual([12, 89]); jest.spyOn(global.Math, 'random').mockRestore(); });
STORE TESTS Actions Keep in mind this sample action // export const actions = { async getRecords (context) { context.commit('getRecordsRequest'); try { const results = await axios.get('/api/records/'); context.commit('getRecordsSuccess', results.data); } catch (error) { context.commit('getRecordsFailure', error); } } // };
STORE TESTS Actions Mocking calls using jest .mockReturnValue(value) for mocking sync results .mockResolvedValue(value) for mocking async results with success .mockRejectedValue(value) for mocking async results with failure import axios from 'axios'; import { actions } from '@/store'; jest.mock('axios', () => ({ get: jest.fn(), })); https://jestjs.io/docs/mock-functions
STORE TESTS Actions Mocking axios success const { getRecords } = actions; test('getRecords success', async () => { const commit = jest.fn(); axios.get.mockResolvedValue({ data: 'ok' }); await getRecords({ commit }); expect(commit).toHaveBeenCalledWith('getRecordsRequest'); expect(axios.get).toHaveBeenCalledWith('/api/records/'); expect(commit).toHaveBeenCalledWith( 'getRecordsSuccess', 'ok'); });
STORE TESTS Actions Mocking axios failures const { getRecords } = actions; test('getRecords failure', async () => { const commit = jest.fn(); axios.get.mockRejectedValue('my error'); try { await getRecords({ commit }); // Fail test if above expression doesn't throw anything expect(true).toBe(false); } catch (error) { expect(commit).toHaveBeenCalledWith('getRecordsRequest'); expect(axios.get).toHaveBeenCalledWith('/api/records/'); expect(commit).toHaveBeenCalledWith( 'getRecordsFailure', 'my error'); } });
COMPONENT TESTS USING ORIGINAL STORE (NOT SUGGESTED) import { shallowMount } from '@vue/test-utils'; import Home from '@/views/Home.vue'; import store from '@/store'; test('snapshot test with default props', () => { const wrapper = shallowMount(Home, { store }); expect(wrapper).toMatchSnapshot(); }); https://vue-test-utils.vuejs.org/guides/using-with-vuex.html
COMPONENT TESTS USING ORIGINAL STORE (NOT SUGGESTED) Pros fast and easy, store implementation ready-to-use Cons less control over store mocking and external calls https://vue-test-utils.vuejs.org/guides/using-with-vuex.html
COMPONENT TESTS MOCKING THE STORE import { shallowMount, createLocalVue } from '@vue/test-utils' import Vuex from 'vuex'; import Home from '@/views/Home.vue'; const localVue = createLocalVue(); localVue.use(Vuex); // ... https://vue-test-utils.vuejs.org/guides/using-with-vuex.html
COMPONENT TESTS MOCKING THE STORE // ... describe('Home.vue', () => { let state, getters, mutations, actions, store; beforeEach(() => { state = { count: 0 }; getters = { getter1: () => 'mocked return value' }; mutations = { mutation1: jest.fn() }; actions = { action1: jest.fn() }; store = new Vuex.Store({ state, getters, mutations, actions }); }); // ... https://vue-test-utils.vuejs.org/guides/using-with-vuex.html
COMPONENT TESTS MOCKING THE STORE // ... test('snapshot test with default props', () => { const wrapper = shallowMount(Home, { store, localVue }); expect(wrapper).toMatchSnapshot(); // ... }); }); https://vue-test-utils.vuejs.org/guides/using-with-vuex.html
RECAP What we learned today? Why use Vuex Install and configure a Vuex store Test store and components
LINKS @dennybiasiolli vuex.vuejs.org vue-test-utils.vuejs.org github.com/dennybiasiolli/bingo-extraction dennybiasiolli.com

State manager in Vue.js, from zero to Vuex

  • 1.
    Introducing Vuex inyour projects How to add and use Vuex in existing projects, with an eye for testing.   - Denny Biasiolli -
  • 2.
    WHO AM I DennyBiasiolli Freelance Full Stack Developer Front End Developer UX/ UI Fingerprint Supervision Ltd Savigliano (CN) - Italy Volunteer in a retirement home, performing recreational activities @dennybiasiolli denny.biasiolli@gmail.com dennybiasiolli.com
  • 3.
  • 4.
  • 5.
    EXAMPLE APP Main component,data() export default { name: 'Home', data() { // component's state return { availableNumbers: [...Array(90).keys()] .map((i) => i + 1), extractedNumbers: [], }; }, // ... };
  • 6.
    EXAMPLE APP Main component,computed export default { // ... computed: { // component's getters ascendingExtractedNumbers() { return [...this.extractedNumbers].sort((a, b) => a - b); }, }, // ... };
  • 7.
    EXAMPLE APP Main component,methods export default { // ... methods: { // component's actions/mutations handleExtract() { const index = Math.floor( Math.random() * this.availableNumbers.length); const extracted = this.availableNumbers .splice(index, 1); this.extractedNumbers = this.extractedNumbers .concat(extracted); }, }, };
  • 8.
    EXAMPLE APP Main componenttemplate <button @click="handleExtract">Extract</button> <h1> Extracted: {{ extractedNumbers[extractedNumbers.length - 1] }} </h1> <DisplayNumbers title="Available numbers" :numbers="availableNumbers" /> <DisplayNumbers title="Extracted numbers" :numbers="ascendingExtractedNumbers" />
  • 9.
    EXAMPLE APP DisplayNumbers component <v-cardelevation="2"> <v-card-title>{{ title }}</v-card-title> <v-card-text> <v-chip v-for="n of numbers" :key="n" class="ma-1"> {{ n }} </v-chip> </v-card-text> </v-card> export default { name: 'DisplayNumbers', props: { title: String, numbers: Array, }, };
  • 10.
    COMPONENT TESTS DisplayNumbers import {shallowMount } from '@vue/test-utils'; import DisplayNumbers from '@/components/DisplayNumbers.vue'; test('renders as expected', () => { const wrapper = shallowMount(DisplayNumbers, { stubs: ['v-container', 'v-card', 'v-card-title', 'v-card-t propsData: { title: 'title text', numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], }, }); expect(wrapper).toMatchSnapshot(); });
  • 11.
    COMPONENT TESTS Home #1 import{ shallowMount } from '@vue/test-utils'; import Home from '@/views/Home.vue'; const shallowMountComponent = () => shallowMount(Home, { stubs: ['v-container', 'v-btn', 'v-row', 'v-col'], }); test('renders as expected', () => { const wrapper = shallowMountComponent(); expect(wrapper).toMatchSnapshot(); }); // ...
  • 12.
    COMPONENT TESTS Home #2 //... test('extracts a number and render as expected', async () => { jest.spyOn(global.Math, 'random') .mockReturnValueOnce(0.123456789) .mockReturnValueOnce(0.987654321); const wrapper = shallowMountComponent(); wrapper.vm.handleExtract(); await wrapper.vm.$nextTick(); expect(wrapper).toMatchSnapshot(); wrapper.vm.handleExtract(); await wrapper.vm.$nextTick(); expect(wrapper).toMatchSnapshot(); jest.spyOn(global.Math, 'random').mockRestore(); });
  • 13.
  • 14.
    STATE FLOW SUMMARY Flowprocess Vue.js component State data and computed View <template> Actions methods
  • 15.
  • 16.
    Solution 1: Movingstate to parent components move data() from Home to App receiving numbers in Home and Footer as props emitting an event when "Extract" button is clicked in Home handling extract event in App component, moving methods from Home to App updating tests
  • 17.
    PROS fast and easyin small apps keep the state in the components where it is used (if there is no need to pass it to other components) no extra dependencies testing sub-components with propsData and snapshots
  • 18.
    CONS multiple views maydepend on the same piece of state actions from different views may need to mutate the same piece of state messy on big apps, lots of extra code for passing props, emitting events hard to follow state changes on many levels what is causing a data change?
  • 19.
    WHAT IS VUEX? Astate management pattern/library for Vue.js applications. It serves as a centralized store for all the components in an application, with rules ensuring that the state can only be mutated in a predictable fashion. https://vuex.vuejs.org/
  • 20.
    WHEN SHOULD IUSE IT? There's a good quote from Dan Abramov, the author of Redux: Flux libraries are like glasses: you’ll know when you need them. https://vuex.vuejs.org/
  • 21.
    WHEN SHOULD IUSE IT? It's a trade-off between short term and long term productivity. If you jump right into Vuex, it may feel verbose and daunting. But if you are building a medium-to-large-scale SPA, chances are you have run into situations that make you think about how to better handle state outside of your Vue components, and Vuex will be the natural next step for you. https://vuex.vuejs.org/
  • 22.
  • 23.
    INSTALL VUEX or <script src="/path/to/vue.js"></script> <scriptsrc="/path/to/vuex.js"></script> npm install --save vuex # or yarn add vuex # https://yarnpkg.com/ # or npx @vue/cli add vuex # https://cli.vuejs.org/ import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); https://vuex.vuejs.org/installation.html
  • 24.
    CONFIGURE VUEX Creating thestore // src/store/index.js import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); export default new Vuex.Store({ state: { /* ... */ }, mutations: { /* ... */ }, }); https://vuex.vuejs.org/guide/
  • 25.
    CONFIGURE VUEX Enabling this.$storeinside Vue components // src/main.js // ... import store from './store'; new Vue({ store, // same as `store: store` // ... }); https://vuex.vuejs.org/guide/
  • 26.
    CONCEPTS: STATE Creation new Vuex.Store({ state:{ count: 0 }, // ... }); https://vuex.vuejs.org/guide/state.html
  • 27.
    CONCEPTS: STATE Basic usage <div> {{$store.state.count }} {{ count }} </div> computed: { count () { return this.$store.state.count; } } https://vuex.vuejs.org/guide/state.html
  • 28.
    CONCEPTS: STATE mapState usage import{ mapState } from 'vuex'; export default { // ... computed: mapState({ count: state => state.count, countAlias: 'count', // to access local state with `this` countPlusLocalState (state) { return state.count + this.localCount; } }) }; https://vuex.vuejs.org/guide/state.html
  • 29.
    CONCEPTS: STATE mapState usagesimplified is the same as mapState({ count: state => state.count }) mapState([ 'count' ]) https://vuex.vuejs.org/guide/state.html
  • 30.
    CONCEPTS: STATE mapState usagewith other computed values computed: { ...mapState({ // ... }), localComputed () { /* ... */ } } https://vuex.vuejs.org/guide/state.html
  • 31.
    CONCEPTS: GETTERS Getters arelike "computed" values for a Vuex store Creation const store = new Vuex.Store({ state: { count: 0 }, getters: { countIsEven: state => { return state.count % 2 === 0; } } }); https://vuex.vuejs.org/guide/getters.html
  • 32.
    CONCEPTS: GETTERS Basic usage <div> {{$store.getters.countIsEven }} {{ countIsEven }} </div> computed: { countIsEven () { return this.$store.getters.countIsEven; } } https://vuex.vuejs.org/guide/getters.html
  • 33.
    CONCEPTS: GETTERS mapGetters usage import{ mapGetters } from 'vuex'; export default { // ... computed: mapGetters({ countIsEvenAlias: 'countIsEven' }) }; https://vuex.vuejs.org/guide/getters.html
  • 34.
    CONCEPTS: GETTERS mapGetters advancedusage computed: { ...mapState(['count']), ...mapGetters(['countIsEven']), localComputed () { /* ... */ } } https://vuex.vuejs.org/guide/getters.html
  • 35.
    CONCEPTS: MUTATIONS Committing amutation is the only way to actually change state in a Vuex store. Creation const store = new Vuex.Store({ state: { count: 0 }, mutations: { increment (state, payload=1) { state.count += payload; } } }); https://vuex.vuejs.org/guide/mutations.html
  • 36.
    CONCEPTS: MUTATIONS Basic usage methods:{ increment (value) { return this.$store.commit('increment', value); } } https://vuex.vuejs.org/guide/mutations.html
  • 37.
    CONCEPTS: MUTATIONS mapMutations usage import{ mapMutations } from 'vuex'; export default { // ... methods: { ...mapMutations([ 'increment' ]), ...mapMutations({ add: 'increment' }) } }; https://vuex.vuejs.org/guide/mutations.html
  • 38.
    MUTATIONS MUST BESYNCHRONOUS Why? Because we need to have a "before" and "a er" snapshots of the state. If we introduce a callback inside a mutation, it makes that impossible. The callback is not called yet when the mutation is committed, and there's no way to know when the callback will actually be called. Any state mutation performed in the callback is essentially un-trackable! https://vuex.vuejs.org/guide/mutations.html
  • 39.
    CONCEPTS: ACTIONS Actions aresimilar to mutations, with a few differences: Instead of mutating the state, actions commit mutations. Actions can contain arbitrary asynchronous operations. https://vuex.vuejs.org/guide/actions.html
  • 40.
    CONCEPTS: ACTIONS Creation const store= new Vuex.Store({ state: { count: 0 }, mutations: { increment (state, payload=1) { state.count += payload; } }, actions: { incrementAsync (context, payload) { setTimeout(() => { context.commit('increment', payload); }, 1000); } } }); https://vuex.vuejs.org/guide/actions.html
  • 41.
    CONCEPTS: ACTIONS API callexample const store = new Vuex.Store({ actions: { async getRecords (context) { context.commit('getRecordsRequest'); try { const results = await axios.get('/api/records/'); context.commit('getRecordsSuccess', results.data); } catch (error) { context.commit('getRecordsFailure', error); } } } }); https://vuex.vuejs.org/guide/actions.html
  • 42.
    CONCEPTS: ACTIONS Context object context.committo commit a mutation context.state access the state context.getters access the getters context.dispatch to call other actions https://vuex.vuejs.org/guide/actions.html
  • 43.
    CONCEPTS: ACTIONS mapActions usage import{ mapActions } from 'vuex'; export default { // ... methods: { incrementAsyncLocal (value) { return this.$store.dispatch('incrementAsync', value) .then( /* ... */); } ...mapActions(['incrementAsync']), ...mapActions({ addAsync: 'incrementAsync' }) } }; https://vuex.vuejs.org/guide/actions.html
  • 44.
    EXAMPLE APP Creating thestore, default state // src/store/index.js import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); export const defaultState = { availableNumbers: [...Array(90).keys()] .map((i) => i + 1), extractedNumbers: [], };
  • 45.
    EXAMPLE APP Creating thestore, getters // src/store/index.js export const getters = { ascendingExtractedNumbers(state) { return [...state.extractedNumbers].sort((a, b) => a - b); }, };
  • 46.
    EXAMPLE APP Creating thestore, mutations // src/store/index.js export const mutations = { extractNumber(state) { const index = Math.floor( Math.random() * state.availableNumbers.length); const extracted = state.availableNumbers .splice(index, 1); state.extractedNumbers = state.extractedNumbers .concat(extracted); }, };
  • 47.
    EXAMPLE APP Creating thestore, composing // src/store/index.js export default new Vuex.Store({ state: defaultState, getters, mutations, });
  • 48.
    EXAMPLE APP Home component,data and computed function @@ src/views/Home.vue - data() { - return { - availableNumbers: [...Array(90).keys()].map((i) => i + - extractedNumbers: [], - }; - }, computed: { - ascendingExtractedNumbers() { - return [...this.extractedNumbers].sort((a, b) => a - b) - }, + ...mapState(['availableNumbers', 'extractedNumbers']), + ...mapGetters(['ascendingExtractedNumbers']), },
  • 49.
    EXAMPLE APP Home component,method @@ src/views/Home.vue - <button @click="handleExtract">Extract</button> + <button @click="extractNumber">Extract</button> methods: { - handleExtract() { - const index = Math.floor(Math.random() * this.available - const extracted = this.availableNumbers.splice(index, 1 - this.extractedNumbers = this.extractedNumbers.concat(ex - }, + ...mapMutations(['extractNumber']), },
  • 50.
    MOVING TO VUEXSTORE FLOW Vue.js component Vuex store Map in data state computed computed getters computed sync methods mutations methods async methods actions methods
  • 51.
    STORE TESTS Default state import{ defaultState } from '@/store'; test('should have the default state', () => { expect(defaultState).toEqual({ availableNumbers: [...Array(90).keys()].map((i) => i + 1), extractedNumbers: [], }); });
  • 52.
    STORE TESTS Getters import {getters } from '@/store'; const { ascendingExtractedNumbers } = getters; test('ascendingExtractedNumbers', () => { expect( ascendingExtractedNumbers({ extractedNumbers: [12, 56, 34] }) ).toEqual([12, 34, 56]); });
  • 53.
    STORE TESTS Mutations import {mutations } from '@/store'; const { defaultState, extractNumber } = mutations; test('ascendingExtractedNumbers', () => { jest.spyOn(global.Math, 'random') .mockReturnValueOnce(0.123456789) .mockReturnValueOnce(0.987654321); const state = { ...defaultState }; expect(state.availableNumbers).toHaveLength(90); // ...
  • 54.
  • 55.
    STORE TESTS Actions Keep inmind this sample action // export const actions = { async getRecords (context) { context.commit('getRecordsRequest'); try { const results = await axios.get('/api/records/'); context.commit('getRecordsSuccess', results.data); } catch (error) { context.commit('getRecordsFailure', error); } } // };
  • 56.
    STORE TESTS Actions Mocking callsusing jest .mockReturnValue(value) for mocking sync results .mockResolvedValue(value) for mocking async results with success .mockRejectedValue(value) for mocking async results with failure import axios from 'axios'; import { actions } from '@/store'; jest.mock('axios', () => ({ get: jest.fn(), })); https://jestjs.io/docs/mock-functions
  • 57.
    STORE TESTS Actions Mocking axiossuccess const { getRecords } = actions; test('getRecords success', async () => { const commit = jest.fn(); axios.get.mockResolvedValue({ data: 'ok' }); await getRecords({ commit }); expect(commit).toHaveBeenCalledWith('getRecordsRequest'); expect(axios.get).toHaveBeenCalledWith('/api/records/'); expect(commit).toHaveBeenCalledWith( 'getRecordsSuccess', 'ok'); });
  • 58.
    STORE TESTS Actions Mocking axiosfailures const { getRecords } = actions; test('getRecords failure', async () => { const commit = jest.fn(); axios.get.mockRejectedValue('my error'); try { await getRecords({ commit }); // Fail test if above expression doesn't throw anything expect(true).toBe(false); } catch (error) { expect(commit).toHaveBeenCalledWith('getRecordsRequest'); expect(axios.get).toHaveBeenCalledWith('/api/records/'); expect(commit).toHaveBeenCalledWith( 'getRecordsFailure', 'my error'); } });
  • 59.
    COMPONENT TESTS USINGORIGINAL STORE (NOT SUGGESTED) import { shallowMount } from '@vue/test-utils'; import Home from '@/views/Home.vue'; import store from '@/store'; test('snapshot test with default props', () => { const wrapper = shallowMount(Home, { store }); expect(wrapper).toMatchSnapshot(); }); https://vue-test-utils.vuejs.org/guides/using-with-vuex.html
  • 60.
    COMPONENT TESTS USINGORIGINAL STORE (NOT SUGGESTED) Pros fast and easy, store implementation ready-to-use Cons less control over store mocking and external calls https://vue-test-utils.vuejs.org/guides/using-with-vuex.html
  • 61.
    COMPONENT TESTS MOCKINGTHE STORE import { shallowMount, createLocalVue } from '@vue/test-utils' import Vuex from 'vuex'; import Home from '@/views/Home.vue'; const localVue = createLocalVue(); localVue.use(Vuex); // ... https://vue-test-utils.vuejs.org/guides/using-with-vuex.html
  • 62.
    COMPONENT TESTS MOCKINGTHE STORE // ... describe('Home.vue', () => { let state, getters, mutations, actions, store; beforeEach(() => { state = { count: 0 }; getters = { getter1: () => 'mocked return value' }; mutations = { mutation1: jest.fn() }; actions = { action1: jest.fn() }; store = new Vuex.Store({ state, getters, mutations, actions }); }); // ... https://vue-test-utils.vuejs.org/guides/using-with-vuex.html
  • 63.
    COMPONENT TESTS MOCKINGTHE STORE // ... test('snapshot test with default props', () => { const wrapper = shallowMount(Home, { store, localVue }); expect(wrapper).toMatchSnapshot(); // ... }); }); https://vue-test-utils.vuejs.org/guides/using-with-vuex.html
  • 64.
    RECAP What we learnedtoday? Why use Vuex Install and configure a Vuex store Test store and components
  • 65.