Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to achieve state normalization in Vuex?

I am developing an app also using Vuex and just hit a situation where dealing with nested objects will be a pain, so I am trying to normalize(flatten) the state as much as possible like the example below:

users: {
    1234: { ... },
    46473: { name: 'Tom', topics: [345, 3456] }
},
userList: [46473, 1234]

My question is: What's the "best" way to achieve the above when your API response looks like this:

data: [
    {id: 'u_0001', name: 'John', coments: [{id: 'c_001', body: 'Lorem Ipsum'}, {...}],
    {id: 'u_0002', name: 'Jane', coments: [{id: 'c_002', body: 'Lorem Ipsum'}, {...}],
    {...}
] 

Assuming for the sake of the example that comments is a submodule of users:

Option 1:

// action on the user module
export const users = ({ commit, state }, users) => {
    commit(SET_USERS, users)
    commit('comments/SET_COMMENTS', users)
}


// mutation on the user module
[types.SET_USERS] (state, users) {
     state.users = users.reduce((obj, user) => {
         obj[user.id] = {
             id: user.id, 
             name: user.name,
             comments: user.comments.map(comment => comment.id)
         } 
         return obj
     }, {})
    state.userIds = users.map(user => user.id)
},


// mutation on the comments module
[types.SET_COMMENTS] (state, users) {
    let allComments = []
    users.forEach(user =>  {
        let comments = user.comments.reduce((obj, comment) => {
            obj[comment.id] = comment
            return obj
        }, {})
        allComments.push(comments)
    })

   state.comments = ...allComments
},

IMO this option is good because you don't have to worry about resetting the state every time you change pages(SPA/Vue-Router), avoiding the scenario where for some reason id: u_001 no longer exists, because the state is overridden every time the mutations are called, but it feels odd to pass the users array to both mutations.

Option 2:

// action on the user module
export const users = ({ commit, state }, users) => {
    // Here you would have to reset the state first (I think)
    // commit(RESET)

    users.forEach(user => {
        commit(SET_USER, user)
        commit('comments/SET_COMMENTS', user.comments)
    })
}


// mutation on the user module
[types.SET_USER] (state, user) {
    state.users[user.id] = {
        id: user.id, 
        name: user.name,
        comments: user.comments.map(comment => comment.id)
    }  
    state.userIds.push(user.id)
},


// mutation on the comments module
[types.SET_COMMENTS] (state, comments) {
    comments.forEach(comment => {
        Vue.set(state.comments, comment.id, comment)
    })
    state.commentsIds.push(...comments.map(comment => comment.id)
},

In this situation there's the need to reset state or you will have repeated/old values every time you leave and re-renter the page. Which is kind of annoying and more susceptible to bugs or inconsistent behaviors.

Conclusion How are you guys tackling such scenarios and advices/best practices? Answers are very appreciated since I'm stuck on these things.

Also, I'm trying to avoid 3r party libraries like the Vue ORM, normalizr, etc. because the needs are not that complex.

Thank you,

PS: The code might have errors since I just wrote it without testing, please focus on the big picture.

like image 625
Helder Lucas Avatar asked Mar 27 '19 22:03

Helder Lucas


People also ask

How do you mutate Vuex state?

Vuex stores are reactive. When Vue components retrieve state from it, they will reactively and efficiently update if the store's state changes. You cannot directly mutate the store's state. The only way to change a store's state is by explicitly committing mutations.

What is the use of mapState in Vuex?

Mapping State Vuex provides a helper function called mapState to solve this problem. It is used for mapping state properties in the store to computed properties in our components. The mapState helper function returns an object which contains the state in the store.

What is normalized state?

A normalized state is a way to store (organize) data. With this way each entity type will have its own place in the store, so that there is only a single point of truth. This practice is the recommended way to organize data in a Redux application as you can read in the Redux recipes.

Does Vuex keep state on refresh?

To persist Vuex state on page refresh, we can use the vuex-persistedstate package. import { Store } from "vuex"; import createPersistedState from "vuex-persistedstate"; import * as Cookies from "js-cookie"; const store = new Store({ // ...


1 Answers

Well, To avoid Accidental Complexity in the state below are the points which need to be taken care while doing state normalization.

As mentioned under official Redux docs

  1. Each type of data gets its own "table" in the state.
  2. Each "data table" should store the individual items in an object, with the IDs of the items as keys and the items themselves as the values.
  3. Any references to individual items should be done by storing the item's ID.
  4. Arrays of IDs should be used to indicate ordering.

Now with the example above, to remove redundancy from the data. You can use each table for each information such as users, comments and so on.

{
  'users': {
    byId : {
      "user1" : {
        username : "user1",
        name : "User 1",
      },
      "user2" : {
        username : "user2",
        name : "User 2",
      },
      ...
    },
    allIds : ["user1", "user2", ..]
  },
  'comments': {
    byId : {
      "comment1" : {
        id : "comment1",
        author : "user2",
        body: 'Lorem Ipsum'
      },
      "comment2" : {
        id : "comment2",
        author : "user3",
        body: 'Lorem Ipsum'
    },
    allIds : ["comment1", "comment2"]
  }
}

By doing so, we can make sure that more components are connected and responsible for looking and maintaining their own data set instead of every component has a large data set and passing data to the chilren component.

UPDATED ANSWER

Since data have been normalize as per the component, pass the entities from the parent component with single action and as a part of normalization, below benefits can be achieved.

  1. Faster data access, no more iterating over arrays or nested objects.
  2. Loose coupling between components.
  3. Each component has its own place in the store, hence there is a single point of truth.

Hope this helps!

like image 81
Varit J Patel Avatar answered Oct 31 '22 11:10

Varit J Patel