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,952