Best Practices

for managing global state with

Vuex

 

Thorsten Lünborg

github.com/linusborg // @linus_borg

whoami

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

  1. Use built-in helpers
  2. 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,635