State management in Vue.js

Vue.js adopts a one-way data flow model, which means that state always flows from the parent components to the child components and is usually passed via props. This is a great way of managing state, because it makes things predictable. Ultimately, state needs to be shared between components. This post will explore ways in which parent and child components can share state and most importantly how child components can pass state back to their parents.

The main rule of thumb is that props should never be mutated and each component should only mutate its own "local" data. As soon as you mutate a prop, which was passed from a parent component you’ll see a warning from Vue, which looks somewhat like this:

[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value.

So let’s keep this in mind, mutating props is a no-go in Vue.js.


Passing state from child back to the parent

Let’s look at an example of a parent & child component with the child affecting the state of the parent.

<template>
  <child-component :message="message" @update="updateMessage"></child-component>
</template>

<script>
import ChildComponent from './components/ChildComponent'

export default {
  name: 'App',
  components: {
    ChildComponent
  },
  data () {
    return {
      message: 'Hello World!'
    }
  },
  methods: {
    updateMessage (message) {
      this.message = message
    }
  }
}
</script>

The ChildComponent will accept data as a prop and then send an event to its parent whenever the local state changes:

<template>
  <div>
    <input v-model="localMessage">
  </div>
</template>

<script>
export default {
  name: 'ChildComponent',
  data () {
    return {
      localMessage: this.message
    }
  },
  props: {
    message: {
      type: String,
      required: true
    }
  },
  watch: {
    localMessage (message) {
      this.$emit('update', message)
    }
  }
}
</script>

When you look at code like this for the first time it seems like there are a lot of steps involved in passing data back to the parent from the child component. But ultimately it’s all about maintainability and avoiding a situation, where the state is mutated in an unpredictable way.

Vue.js has a useful .sync modifier that helps to shorten the code quite nicely in many situations.


Sync modifier

Let’s see how we can use the .sync modifier to sync data with the parent:

<template>
  <child-component :message.sync="message"></child-component>
</template>

Child component:

<template>
  <div>
    <input v-model="localMessage" @input="$emit('update:message', localMessage)">
  </div>
</template>

The .sync modifier allows us to skip writing the 'updateMessage' method entirely, which only purpose was to sync state received from the child component with the parent’s state. Also notice how I bind to the 'input' event and emit the 'update:message' event in the child component. This may seem like a great idea, but a dedicated watcher is very often a better choice. If 'localMessage' gets modified in some other way than by changing the value of the input field the 'update:message' event won’t get emitted.


Warning: antipattern ahead 🙈

Let’s consider one more way of updating parent’s state from the child component, which I consider to be an antipattern and don’t really encourage anyone to use.

Parent component:

<template>
  <child-component :messages="messages"></child-component>
</template>

<script>
import ChildComponent from './components/ChildComponent'

export default {
  name: 'App',
  components: {
    ChildComponent
  },
  data () {
    return {
      messages: {
        helloWorld: 'Hello World!'
      }
    }
  }
}
</script>

Child component:

<template>
  <div>
    <input v-model="messages.helloWorld">
  </div>
</template>

<script>
export default {
  name: 'ChildComponent',
  props: {
    messages: {
      type: Object,
      required: true
    }
  }
}
</script>

As you see we’ve "cheated the system" by passing an object as a prop to the child component. This lets the child component modify the prop and affect parent’s state directly. Currently Vue cannot detect mutations to object properties, so you won’t see the typical “Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders" warning from Vue. This doesn’t make this method of passing state between parent and child components valid and you should avoid it as much as possible as it’s essentially a hack. 


Deeply nested components

Using events we can pass state from deeply nested children to the top-level component by including an 'updateMessage' method in all the intermediary components that emits an 'update' event and passes state one level up.

<template>
  <grandchild-component :message="message" @update="updateMessage"></grandchild-component>
</template>

<script>
import GrandchildComponent from './GrandchildComponent'

export default {
  name: 'Child',
  components: {
    GrandchildComponent
  },
  ...
  methods: {
    updateMessage (message) {
      // By emitting the 'update' event in every intermediary component we can pass data
      // from GrandchildComponent to ChildComponent and from there to the parent
      this.$emit('update', message)
    }
  }
}
</script>

However, the event-based approach has its limitations and for the more complex component trees it becomes preferable to switch to Vuex for state mangement. I’m going to look into Vuex and how it can help manage state in part 2 of this blog post. 

Did you like this post?
Previous post
Sometimes when passing an object as a prop to a Vue component it’s important to check if it contains all expected properties. In Vue.js this can be accomplished by writing a custom validator function.
Next post

I'm interested in functional programming lately and was happy to discover that modifying array keys in PHP can be done avoiding any kind of loops. 😃