Best Practices
for managing global state with
Vuex
Thorsten Lünborg
github.com/linusborg // @linus_borg
whoami
- Vue core team member since ~06/2016
- "That guy on the forum" answering your questions
- Programming is not my job, but my passion
- https://www.github.com/linusborg/portal-vue
So what is Vuex?
State management pattern + Library
Centralised store
Predictable state mutations
Integrates with Vue Devtools
Time travelling
State snapshots
"Flux"
A Basic Example
(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
'mkaaay .....
...that's a lot of boilerplate to
increase a freakin' counter!
Why?
Why should I use it, anyway?
- Share state between distant component
- Centralize async operations on data (API)
- Enable better tracking of state changes across your application
Dev Tools integration
Track state changes
Revert changes
Travel back & forth in time
...and when should I use it?
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
...like?
global loading indicators
notifications
login status & the current user
posts & comments in a nested view for a blog
- comment count above the post,
- comments list below it
- top comment showing in the sidebar
Start local, then go global
Don't "optimize" prematurely
Start with component state
move parts of the state to vuex when necessary
Best Practices
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
Don't treat it like a database
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
Use (namespaced) modules
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.
What a module looks like:
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
- state
- actions
- mutations
- getters
`namespaced: true`
creates a namespaced module - always use that!
and how to use it:
// 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
- state
- actions
- mutations
- getters
Keep boilerplate in check
- Use built-in helpers
- Write your own custom helpers/ re-use patterns
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')
}
Custom Helpers / re-use patterns
"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
Form Handling
<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!
..seriously?
Well, yes.
but no, not really
so let's improve this step by step
Step 1: "Smart Mutation"
<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?
Step 2: Using computed setters
<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?
Step 3: a smart helper
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">
Usage:
Best of both worlds:
- Using v-model
- No boilerplate
How to structure your data
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
Modularize by Router-View first
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
Normalize your data
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
What are the advantages?
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)
Small Tipps & Tricks
sold out
Some useful libraries
building an ORM on top of vuex:
advanced mapping helpers:
Synching Route State to Vuex
Questions?
now, or visit me at the forum later:
Best Practives for Vuex
By Thorsten Lünborg
Best Practives for Vuex
as presented at Karlsruhe VueJS Usergroup
- 3,801