Vue.js State Management vs. Clean Architecture

Being independent of frameworks is an important attribute of clean architecture. This post explores the challenges of managing the application state in a way that is less specific to Vue.js.

This is how Ucle Bob explains it in The Clean Architecture:

The architecture does not depend on the existence of some library of feature laden software. This allows you to use such frameworks as tools, rather than having to cram your system into their limited constraints.

This means you should not need to be writing the business logic of your application in a framework prescribed way.

Vue.js basics

Vue.js is a JavaScript framework for building user interfaces for the web. At the heart of the framework lies a reactivity system that maps the component state to rendered DOM.

The example snippet below renders <b>Hello Vue!</b> and if the value of message changes the rendered element gets updated as well:

new Vue({
  data: {
    message: 'Hello Vue!'
  },
  template: '<b>{{ message }}</b>'
})

Reactive state limitations

Any property under data becomes a reactive property when the Vue instance initializes. One might think that this means data can hold arbitrary data structures which can be manipulated any way we like. Unfortunately that is not true, there are limitations:

I have not found a single authoritative source explicitly listing which types are supported and which are not as data properties in Vue.js. It seems like the best approach is to stick to using “basic” types (Array, String, Symbol, Number, Object, Boolean and undefined) only and avoid anything else (no custom prototypes, no Date or other language or browser built-in types).

So where is the problem?

The limitations on what types and what methods Vue allows to use might not appear to be a big deal for small and simple use cases. A real life application might eventually reach a point where there appears to be a piece of logic that is independent from the presentation and will benefit from encapsulation - this logic is often called business logic.

The clean architecture approach recommends a business logic implementation to be implemented in a framework independent way but Vue’s constraints mean we can not leverage the full power of modern JavaScript (or whatever language we compile to JavaScript). When implementing data structures or algorithms to be used for representing or manipulating component state, we have to play by the Vue.js rules.

An example

Let’s say we want to implement an application that shows a timeline and allows adding events to it:

<ul id="app">
  <li v-for="entry in timeline">
    {{ entry.year }}: {{ entry.event }}
  </li>
  <li>
    <input type="number" v-model="year">:
    <input type="text" v-model="event">
    <button @click="addEvent">Add event</button>
  </li>
</ul>

<script>
  new Vue({
    el: '#app',
    data: {
      year: 2018,
      event: 'Someone clicked a button',
      timeline: [
        {year: 1946, event: 'ENIAC'},
        {year: 1981, event: 'IBM PC'},
        {year: 2007, event: 'iPhone'}
      ]
    },
    methods: {
      addEvent (event) {
        this.timeline.push({
          year: this.year,
          event: this.event
        })
      }
    }
  })
</script>

This simple example works as expected as we are mutating the state in a Vue.js compliant way.

Extracting the business logic

As the application evolves we might realize that we often have to add events to the timeline, we might want to prevent certain operations on the timeline array or make sure it is always sorted. This will result in extracting a Timeline class, something like this:

function Timeline (initialEntries = []) {
  // Keep the cloned entries array private
  const entries = initialEntries.slice()

  return {
    add (year, event) {
      entries.push({year, event})
      entries.sort((a, b) => a.year - b.year)
    }
  }
}


// Create a timeline with some entries
new Timeline([
  {year: 1946, event: 'ENIAC'},
  {year: 1981, event: 'IBM PC'},
  {year: 2007, event: 'iPhone'}
])

By wrapping the array and keeping it private we can add our own manipulation methods which can ensure the structure is solid.

However if we substitute the plain array with a Timeline in Vue data it will no longer work. Vue has no idea how to iterate over a custom class, we have to convert it to an array:

function Timeline (...) {
  return {
    // ...
    toArray () {
      // Clone the private array
      return entries.slice()
    }
  }
}

An even cleaner option would be implementing a custom iterable but that is not something Vue can directly use.

Calling toArray() from v-for will have the desired results:

<li v-for="entry in timeline.toArray()">

We are less lucky however when it comes to adding entries to the timeline. Even if we replace push with timeline.add this will not result in updating the rendered DOM:

methods: {
  addEvent (event) {
    this.timeline.add(this.year, this.event)
  }
}

Looks like we reached a dead end and in fact we have been told to only use “basic” types and don’t even try to add our own methods.

The way forward

Turns out the workaround is simple: instead of updating an object assigned to a data property in-place, entirely replacing the instance does kick off Vue’s reactive updates:

methods: {
  addEvent (event) {
    // Clone the timeline
    const timeline = new Timeline(this.timeline.toArray())
    // Add an entry to the new timeline
    timeline.add(this.year, this.event)
    // Replace this.timeline with the new instance
    this.timeline = timeline
  }
}

Sharp-eyed readers might notice that at this point we could implement Timeline as an immutable data structure as there will never be a need for in-place updates.

This approach has some downsides though:

Conclusion

Your app might be simple enough or 99% of the business logic lives on the server so that the Vue.js constraints discussed here have no big impact on the architecture.

If you are building something more complicated, my recommendation is the following: