Using normalizr to work with nested data in Vue & Vuex

Development

Compared to older frameworks, developing an application in Vue is a real treat. However, even when working in Vue and Vuex, things can quickly become complicated. Especially when working with state objects that go multiple levels deep. I ran into this problem while developing and it has costed my quite some time to figure out an elegant solution for this. Since I haven’t been able to find proper examples on this online, I want to share my experiences and some code here.

the problem

When data is returned from an api, it might contain multiple entities that you want to work with separately. For example, when making an online quiz application, your data might look like this:


{
  "rounds": [{
   "Round name": "World questions!",
   "description": "Questions from around the world!",
   "questions": [
     {
     "name": "How tall is the Eifel Tower?",
     "answers": { 1: "50m", 2: "30m", 3: "10m" }
     },
      // More questions
    ]
  },
  {
    "Round name": "Second round",
    // etcetera...
  }]
}

For this specific example it might not be that clear what the problem is. But say, we want to re-use the same question over multiple rounds? It is possible to literally copy the question object to each round object, But this means updating this question later one will require us to traverse through all the rounds and search and replace the question. Instead, we want each round to contain references to the questions, so we can easily reuse the questions in multiple rounds.

The solution

A possible solution lies in normalizing your data, which basically means converting it from a nested object, to grouping it by data type. If your data is a list of quiz rounds, each containing questions, with each question containing multiple answers. This is a typical example of nested data. The nested “shape” of data works fine for rendering html when passed to a Vue component, but updating in Vuex might become a problem.

Vuex works best with flat data, therefore it’s important to keep our store as flat as possible. We use normalizr to flatten api responses to multiple entities and update those in the store.

Normalizr

Normalizr is a framework agnostic library build for building flat object structures from nested ones. It does so by looping through an object according to a scheme. For this to work, a scheme must be defined in which we define the relationship between the various nested entities. In the quiz example these entities would be:

  • Rounds, containing multiple questions
  • Questions, containing multiple answers
  • Answers

A Scheme for normalizr could then look like this:


// Import normalizr library
import {schema} from 'normalizr'

// Defines the entity of answers
export const answer = new schema.Entity('answers')

// Defines the entity of questions, containing an array of answers
export const question = new schema.Entity('questions', {
answers: [ answer ]
})

// Defines the entity of rounds, containing an array of questions
export const round = new schema.Entity('rounds', {
questions: [ question ]
})

Next, what we can do to turn nested JSON into normalized JSON is the following:


import { round } from './schema'
const normalizedData = normalize(originalData, round);

This will result in an object that contains references to the entities instead of the complete ones:

Integration with Vuex

So how does flattening our data help in Vuex? Vuex store mutations become quite complex when using nested data, since they need to be able to access all parent entities from root to target in order to update just one.
For example, updating an answer requires vuex to go: store -> rounds(id) -> questions(id) -> answer(id).
When data is normalized however, the answer can be updated simply by following the path: store -> answer(id).

Since all of our entities are now being saved in a similar way, it becomes quite easy to create a generic action and mutation for updating your entities in the store, based on an API response for example like this:


import normalize from 'normalizr'
import Vue from 'vue'
import api from 'your-api'
import schema from 'your-schema'

const actions = {
  // Based on api response, update the returned entities in our store
  updateEntity ({ commit }, {id, fields}) {
    return api.update(id, fields)
    .then(resp => {
      const entity = resp.data.data
      // Normalizing allows us to break down the response into separate 'entities'
      const normalizedEntity = normalize(entity, schema)
      commit(types.UPDATE_ENTITIES, { entities: normalizedEntity.entities })
    })
}

const mutations = {
  [types.UPDATE_ENTITIES] (state, { entities }) {
  // Loop over all kinds of entities we received
  for (let type in entities) {
    for (let entity in entities[type]) {
      const oldObj = state.entities[type][entity] || {}
      // Merge the new data in the old object
      const newObj = Object.assign(oldObj, entities[type][entity])
      // Make sure new entities are also reactive
      Vue.set(state.entities[type], entity, newObj)
    }
  }
  }
}

By implementing these two functions your application will not magically work with normalized data. However, if done correctly, normalizing data can save you a lot of time when your application grows more and more complex.