for managing global state with
Thorsten Lünborg
github.com/linusborg // @linus_borg
State management pattern + Library
Centralised store
Predictable state mutations
Integrates with Vue Devtools
Time travelling
State snapshots
(very)
import Vue from 'vue'
import Vuex from 'Vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
counter: 0,
}
mutations: {
INCREASE: (state, payload) => state.counter = state.counter + payload,
DECREASE: state => state.counter++
},
actions: {
increase: ({ commit }, payload) => commit('INCREASE', payload)
decrease: ({ commit }) => commit('DECREASE')
}
// getters: {} left out for now
})
new Vue({
el: '#app',
store,
methods: {
increase(value) { this.$store.dispatch('increase', value) }
decrease() { this.$store.dispatch('decrease') }
}
})
Setup
...that's a lot of boilerplate to
increase a freakin' counter!
Track state changes
Revert changes
Travel back & forth in time
you will know when you need it
@thoughtleader_on_twitter
It's not very useful in small applications
But it might be useful sooner than you might think
You also don't have to use it for *all* of your state
Just the state that has to be global
global loading indicators
notifications
login status & the current user
posts & comments in a nested view for a blog
Don't treat it like a database
Use (namespaced) modules
Keep boilerplate in check
Form Handling
How to structure your data
Small Tips & tricks
Great libs to help you
Advanced
it's a state manager, not a datastore/ORM/ember-data
Don't re-build your database locally
Don't use it like a cache, get fresh data on every view
Keep store structure view-centric (i.e. per route)
...but manage long-lived data separately
Vuex stores can be split up into modules
modules can be nested in other modules
This allows you to encapsulate a part of the state
Namespacing avoids naming collisions and gives structure to your action/mutation/getter calls.
export default {
// important!
namespaced: true
state: {
myModuleData: []
},
actions: {
coolAction: ({commit}) => { /*...*/ }
},
mutations: {
// ...
},
getters: {
moduleGetter: (state) => { /* ... */ }
},
modules: {
// nest other modules here!
}
}
Modules look just like the main store
`namespaced: true`
creates a namespaced module - always use that!
// store.js
import myModule from './my-module.js'
export default new Vuex.Store({
state: {}
actions: {}
mutations: {},
modules: {
myModule
}
})
// in a component:
methods: {
callModuleAction: {
this.$store.dispatch('myModule/coolAction')
},
},
computed: {
getModuleGetter() {
return this.$store
.getters['myModule/moduleGetter']
}
}
Modules look just like the main store
without helpers
export default {
// other component options
computed: {
myFirstGetter() {
return this.$store
.getters['myModule/myFirstGetter']
},
mySecondGetter() {
return this.$store
.getters['myModule/mySecondGetter']
}
},
methods: {
go(value) {
this.$store
.dispatch('myModule/myFirstAction', value)
}
}
}
Repetition!
import { mapActions, mapGetters } from 'vuex'
export default {
// other component options...
computed: {
...mapGetters('myModule', [
'myFirstGetter', 'mySecondGetter'
])
},
methods: {
...mpaActions('myModule', {
// rename with an object instead of array
go: 'myFirstAction',
cancel: 'mySecondAction'
})
}
}
with Helpers
less repetition
less typing
less bugs
Namespaced helpers
// post.helpers.js
import { createNamespacedHelpers } from 'vuex'
const UserHelpers = {
...createNamespacedHelpers('post')
}
export default UserHelpers
// Usage:
import { mapActions } from './post.helpers.js'
methods: {
...mapActions(['add'])
// => dispatch('post/add')
}
"Smart" Mutations
dynamic Getters
reusable Modules
"Smart" Mutations
const state = {
profile: {
name: 'Bob',
age: 100,
city: 'Springfield',
// ... a LOT of props
}
}
const mutations = {
UPDATE_PROP: (state, {key, value}) => {
state.profile[key] = value
}
}
Great if many small, simple updates required
Don't overdo it - a supersmart mutation will make your devtools log pretty useless
Dynamic Getters
const getters = {
getCommentById: state => id => state.comments[id]
}
Returns a function that you can call with an argument
allows to query the store for data dynamically (i.e. in a for loop)
can't be cached like normal getters, so don't do expensive calculations in these
Reusable modules
// baseModule.js
export default (name) => ({
namespaced: true,
state: {
entities: {}
},
mutations: {
ADD: (state, payload) => /*...*/,
},
actions: {
add: ({commit}, payload) => {
api[`add${name}`](payload)
.then(() => {
commit('ADD', payload)
}
}
},
//...
}
import baseModule from './baseModule.js'
new Vuex.Store({
modules: {
posts: baseModule('post'),
articles: baseModule('article'),
}
})
Great to write reusable CRUD boilerplate
<input v-model.trim="user.name" type="text">
Vue has these beautiful form helpers
<input
v-bind:value="user.name"
v-on:input="saveUserName"
type="text">
<script>
methods: {
saveUserName(e) {
this.$store.dispatch('saveUserName', e.target.value)
}
}
</script>
But in Vuex, we can't use them!
We have to do this!
Well, yes.
but no, not really
so let's improve this step by step
<input
v-bind:value="user.name"
v-on:input="saveProp('name', $event)"
type="text">
<script>
methods: {
saveProp(prop, e) {
this.$store.dispatch('saveProp', {
key: prop,
value: e.target.value
})
}
}
</script>
Great, now we only need one action!
...but can we do better?
<input
v-model.trim="userName"
type="text">
<script>
computed: {
userName: {
get() { return this.user.name },
set(value) {} this.$store.dispatch('saveProp', {
key: 'name',
value: value
})
}
}
</script>
Now we can use v-model and its cool modifiers again!
...but we need to write a computed get/set per prop?
function mapPropsModels (props = [], { object, action } = {}) {
return props.reduce((accumulator, prop) => {
const propModel = prop + 'Model'
const computedProp = {
get() {
return this[object][prop]
},
set(value) {
this.$store.dispatch(action, { key: prop, value })
}
}
accumulator[propModel] = computedProp
return accumulator
}, {})
}
<script>
computed: {
...mapGetters('auth', {
user: 'currentUser'
}),
...mapProps(['name', 'age', 'city'], {
object: 'user', action: 'saveProp'
})
}
</script>
<input v-model.trim="nameModel" type="text">
<input v-model.number="ageModel" type="text">
<input v-model="cityModel" type="text">
Best of both worlds:
Use modules
Modularize by Router-View first
Move reusable data/logic into their own module
Nest modules, not data
Normalize nested data instead, denormalize when needed
Profile
Newsfeed
Events list
One Store module for each page
All pages have users, but only to display them
Don't over-"optimize" by moving users to their own module
Users
Comments
Alle pages will interact heavily with comments
This logic should be moved to its own module
Just keep List of ids in view
Nest modules, but not data - and generally, don't nest much at all. Keep it flat.
Don't nest modules more than two levels
Nested data is easy to read...,
...but a nightmare to update.
Keep your objects in maps,
keep ids in arrays for listing/sorting
{
"id": "123",
"author": {
"id": "1",
"name": "Paul"
},
"title": "My awesome blog post",
"comments": [
{
"id": "324",
"commenter": {
"id": "2",
"name": "Nicole"
}
}
]
}
{
result: "123",
entities: {
"articles": {
"123": {
id: "123",
author: "1",
title: "My awesome blog post",
comments: [ "324" ]
}
},
"users": {
"1": { "id": "1", "name": "Paul" },
"2": { "id": "2", "name": "Nicole" }
},
"comments": {
"324": { id: "324", "commenter": "2" }
}
}
}
nested data
normalized data
Every object is easily reachable
const author = this.$store.state.myModule.users[post.authorId]
Updating or replacing an object in a list doesn't require to loop through long arrays
getters: {
sortedUsers: state => {
return state.userList.map(id => users[id]
}
}
a sorted user list is just a getter away
(dynamic ones are extra useful here)
sold out
building an ORM on top of vuex:
advanced mapping helpers:
Synching Route State to Vuex
now, or visit me at the forum later: