by Gerard Sans |  @gerardsans

Vuex Modules to the edge

Vuex Modules to the edge

SANS

GERARD

Google Developer Expert

Google Developer Expert

International Speaker

Spoken at 101 events in 27 countries

Blogger

Blogger

Community Leader

900

1.5K

Trainer

Master of Ceremonies

Master of Ceremonies

FREE 3h Workshop

bit.ly/cfp-buenos-aires

Vuex

  • v3.0.1 (Feb 2016)
  • 218 contributors
  • CLI integration
  • 16K stars
  • v6.0.1 (Dec 2015)
  • 126 contributors
  • CLI integration
  • 3K stars
  • v4 (June 2015)
  • 603 contributors
  • Inspired by Flux
  • 42K stars

Vuex

Overview

  • State Management for Vue
  • Inspired by Redux
  • Composable using Modules
  • Vue DevTools integration

Components

Actions

Mutations

commit

dispatch

mutate

State

update

Vuex one-way data flow

Actions

(async)

Mutations

(sync)

Actions vs Mutations

DevTools

Backend

commit

Packages

vue devtools

vuex

vuex-router-sync

Features

Utilities

Counter

Increment

Decrement

Reset

Total

import Vue from "vue";
import App from "./App";

import store from "./store";

new Vue({
  el: "#app",
  store,
  components: { App },
  template: "<App/>"
});
src/app.component.ts

Store Setup

src/main.js

Components

Actions

Mutations

commit

dispatch

mutate

State

update

import Vue from "vue";
import App from "./App";
import store from "./store";

new Vue({
  el: "#app",
  store,                    // this.$store.state
  components: { App },
  template: "<App/>"
});
src/app.component.ts

Store Setup

src/main.js
import actions from "./actions";
import mutations from "./mutations";

export default new Vuex.Store({
  actions, 
  mutations, 
  state, 
});
src/app.component.ts

State

src/store/index.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);

const state = {
  counter: 0
};

export default new Vuex.Store({
  state, 
});
import mutations from "./mutations";
import actions from "./actions";

export default new Vuex.Store({
  state,
  mutations,
  actions,
});

Components

Actions

Mutations

commit

dispatch

mutate

State

update

export default {
  increment(context) {
    context.commit("INCREMENT");
  },
  decrement({ commit }) {
    commit("DECREMENT");
  },
  reset({ commit, dispatch, state, getters }) {
    context.commit("RESET");
  }
};
src/app.component.ts

Counter Actions

src/store/actions.js

Components

Actions

Mutations

commit

dispatch

mutate

State

update

export default {
  INCREMENT(state) {
    state.counter += 1;
  },
  DECREMENT(state) {
    state.counter -= 1;
  },
  RESET(state) {
    state.counter = 0;
  }
};
src/app.component.ts

Counter Mutations

src/store/mutations.js

Components

Actions

Mutations

commit

dispatch

mutate

State

update

<Counter
  @increment="increment" @decrement="decrement" @reset="reset"
/>

export default {
  methods: {
    increment() {
      this.$store.dispatch("increment");
    },
    reset() {
      this.$store.dispatch("reset", { value: 0 });
    }
  },
}
src/app.component.ts

Dispatching Actions

src/App.vue

We pass events up to container

<Counter :total="total" 
  @increment="increment" @decrement="decrement" @reset="reset"
/>

export default {
  computed: {
    total() {
      return this.$store.state.counter;
    }
  }
}
src/app.component.ts

Updating Component

src/App.vue

We will improve this part later

<div>
  <div class="total">{{total}}</div>
  <button @click="$emit('increment')">+</button>
  <button @click="$emit('decrement')">-</button>
  <button @click="$emit('reset', { value:0 })">C</button>
</div>

export default {
  name: "Counter",
  props: ["total"]
};
src/app.component.ts

Counter

src/components/Counter.vue
// src/store/actions.js
reset({ commit }, payload) {
  commit("RESET", payload);
}

// src/store/mutations.js
RESET(state, { value }) {
  state.counter = value;
}
src/app.component.ts

Adding payload

Getters

State

Property

Getters

Computed

Getters

s

g

g

Vuex Getters

  • Helpers to access Store

  • Avoid Components tight coupling with Store

  • Memoized for performance

import getters from "./getters";

export default new Vuex.Store({
  actions, 
  mutations, 
  getters,
  state, 
});
src/app.component.ts

Vuex Store

src/store/index.js
src/app.component.ts

Getters

src/store/getters.js
export default {
  total: state => state.counter,

  overflow: state => {
    return state.counter > state.maximum;
  }
};
this.$store.state.counter;
this.$store.state.pagination.page;

Tight coupling

Components

State

Getters: loose coupling

this.$store.getters.total;
this.$store.getters.page;

Components

getters

State

A small change  won't break all Components now

export default {
  total: state => {
    return state.counter;
  },
  page: state => {
    return state.pagination.page;
  }
};
src/app.component.ts

Property Getters

src/store/getters.js

State

Property

Getters

Computed

Getters

s

s

s

Computed Getters

  • Computed values, can use other getters

  • Memoised for performance

  • Default one-slot memoisation

// State
{ 
  todos: [{ id: 1, text: 'Learn Vuex', complete: false }],
  currentFilter: "SHOW_ALL"
}

// Visible Todos
[{ id: 1, text: 'Learn Vuex', complete: false }]
src/app.component.ts

Example: visibleTodos Getter

// State
{ 
  todos: [{ id: 1, text: 'Learn Vuex', complete: false }],
  currentFilter: "SHOW_COMPLETED"
}

// Visible Todos
[]
const getters = {
  todos: state => state.todos,
  currentFilter: state => state.currentFilter,
};
const getters = {
  todos: state => state.todos,
  currentFilter: state => state.currentFilter,
};

Todos Getters

src/store/getters.js

Getter: Visible Todos

const getters = {
  visibleTodos: function(state, getters) {
    var todos = getters.todos.slice().reverse();
    switch (getters.currentFilter) {
      case "SHOW_ACTIVE":
        return todos.filter(t => !t.done);
      case "SHOW_COMPLETED":
        return todos.filter(t => t.done);
      case "SHOW_ALL":
      default:
        return todos;
    }
  }
};
src/store/getters.js
<todo-list 
  :todos="visibleTodos" :currentFilter="currentFilter"
/>
export default {
  computed: {
    visibleTodos() {
      return this.$store.getters.visibleTodos;
    },
    currentFilter() {
      return this.$store.getters.currentFilter;
    }
  }
};
src/app.component.ts

Using visibleTodos

src/App.vue
<todo-list 
  :todos="visibleTodos" :currentFilter="currentFilter"
/>
export default {
  computed: {
    ...mapGetters(['visibleTodos', 'currentFilter'])
  }
};

ComponentHelpers

import { mapState } from 'vuex';
export default {
  data: () => ({ 
   factor: 12 
  }),
  computed: {
    ...mapState(['total']),
    ...mapState({ counter: 'total' }),
    formula(state) { 
      return state.total*this.factor;
    }
  }
}
src/app.component.ts

Mapping Helpers: mapState

import { mapActions, mapMutations, mapGetters } from 'vuex';

export default {
  methods: {
    ...mapActions(['increment', 'decrement']),
    ...mapActions({ clear: 'reset' }),  // alias
  computed: {
    ...mapGetters({ total: 'total' })
  }
}
src/app.component.ts

Other mapping helpers

Modules

Namespaced Module

Extension

Module

Root

Module

Modules

import cart from './modules/cart'
import products from './modules/products'

export default new Vuex.Store({
  modules: {
    cart,
    products
  },
})
src/app.component.ts

Store Setup

src/store/index.js
export default {
  namespaced: true,
  state: {
    items: [],
    checkoutStatus: null
  },
  actions,
  mutations
  getters,
}
src/app.component.ts

Cart Module

src/store/modules/cart.js
export default {
  namespaced: true,
  state: {
    all: [],
  },
  actions,
  mutations
  getters,
}
src/app.component.ts

Products Module

src/store/modules/products.js
<Product v-for="product in products"
  @click="addProductToCart(product)" 
/>

export default {
  computed: mapState({
    products: state => state.products.all
  }),
  methods: mapActions('cart', [
    'addProductToCart'
  ]),
  created () {
    this.$store.dispatch('products/getAllProducts')
  }
}
src/app.component.ts

ProductList Component

Reusing stores

{ 
  counter: 7
}

Single Counter

dispatch("increment");

State

Action

User clicks

{ 
  counter: 8
}

Mutation

commit("increment");

Multiple Counters

{ 
  "a": { counter: 7 },
  "b": { counter: 3 },
}

a

b

State

User clicks

{ 
  "a": { counter: 7 },
  "b": { counter: 2 },
}
dispatch('b/decrement');

Action

Mutation in Module B

commit("decrement");
import counter1 from "./modules/counter";
import counter2 from "./modules/counter";

export default new Vuex.Store({
  modules: {
    a: counter1,
    b: counter2
  }
});
src/app.component.ts

Store Setup

src/store/index.js
export default {
  namespaced: true,
  state: () => ({
    counter: 0
  }),
  // same actions, mutations and getters
};
src/app.component.ts

Store Setup

src/store/modules/counter.js

Dynamic Modules

import { sync } from 'vuex-router-sync'
import store from './vuex/store' 
import router from './router' 

// to register router module
const unsync = sync(store, router)

// to unregister router module
unsync() 
src/app.component.ts

Vuex Router Sync

Store

Route

sync()

unsync()

state.registerModule('route', store)

state.unregisterModule('route')

Root Registration

Store

my_module

register

unregister

state.registerModule(path, store)

state.unregisterModule(path)

1

path = ['1', 'my_module']

Nested Registration

Router sync

vuex-router-sync/src/index.js
exports.sync = function (store, router, options) {

  store.registerModule('route', {
    namespaced: true,
    state: cloneRoute(router.currentRoute),
    mutations: {
      'ROUTE_CHANGED' (state, t) {
        store.state['route'] = cloneRoute(t.to, t.from)
      }
    }
  })
  ...
}

Router changes

vuex-router-sync/src/index.js
exports.sync = function (store, router, options) {
  // watch state changes 
  const storeUnwatch = store.watch(
    state => state['route'],
    route => router.push(route),
    { sync: true }
  )
  // update state after navigations
  const afterEachUnHook = router.afterEach((to, from) => {
    store.commit('route/ROUTE_CHANGED', { to, from })
  })

  return function unsync() {}
}
exports.sync = function (store, router, options) {

  return function unsync () {
    storeUnwatch()
    afterEachUnHook()
    store.unregisterModule('route')
  }
}

Router unsync

vuex-router-sync/src/index.js

More

@sarah_edo

Blake Newman

Sarah Drasner

@blakenewman

Vuex Modules to the edge

By Gerard Sans

Vuex Modules to the edge

State Management is key to build modern Web Apps

  • 3,747