Vuex best practices for complex objects


#1

My Vuex store contains objects with a variety of complexity. Some have nested objects, some have arrays of nest objects.

I could create generic function to mutate a specified property along the lines of:

setProperty(state,{ type, id, prop, value })
{
state[type][id][prop] = value;
}
but that will quickly get complicated for nested object, arrays of objects. It also seems very tedious to have to create a mutation for every single object property, nested or otherwise.

What are the best practices for creating mutations to modify objects, nested objects, arrays, etc?

Another related issue, is it considered bad form to pass the objects into the mutations as opposed to looking them up in the state:

setProperty(state,{ obj, prop, value })
{
obj[prop] = value;
}


Manage component UI-state on relative complex component that contains nested components
Improving performance of deeply nested objects by normalizing data
Passing Object to child and save the changes in parent component
Vuex practices for nested structures
#2

The best thing to do is to avoid nested structures. This is an established pattern in other global state single state tree systems like redux, was well.

So what to do instead?

Normalize your data:

{
  users: {
    1234: {... }, // ids as keys
    46473: { name: 'Tom', topics: [345, 3456] } // keep only ids of nested items.
  },
  userList: [46473, 1234] // keep array of the ids as well for easy sorting.

  topics: { // nested items have a collection at the top-level as well  -> no nesting.

    345: { ... }, // same thing: keep objects in a collection with the id for the key.
    3456: { ... }
  },
}

This also means that you don’t have to write lookup functions with find() on arrays anymore - if you have an item’s id, you can get it from the collection object immediatly.

If you want an array of user objects, a simple getter is your friend:

getters: {
  userSet: (state) => state.userList.map( userId => state.users[userId] )
  // => [{name: 'Tom' ...}, .... ]
}

And if e.g. you need to access nested data, e.g. the topics for a each user in a component:

computed: {
  users: () => this.$store.getters.userSet,
  topics: () => this.$store.state.topics,
},
methods: {
  userTopics(user) { // this could also be done for the whole user collection in the getter we created above.
   return user.topics.map(topicId => this.topics[topicId]) 
 }
}
<div v-for="user in users">
  <ul>
    <li v-for="topic in userTopics(user)">{{topic.title}}</li>
  </ul>
</div>

It seems like it’s more complicated, and in a way it is, a little. But the advantage is that for updates, you will not have to worry about any nesting-related issues when wrining mutations.

But my API send me nested data, do I have to convert it everytime?

There’s a great little library that is kind of standard in the react-redux community: normalizr. It makes it easy to normalize a nested API response into a data structure like the one in my example.


Vuex form best practices
#3

Thanks for the excellent reply. I have already normalized my data but what about mutations for the properties of the objects? Do you write a mutation for every property of every object type or do you use something generic like my setProperty() mutation above?

What if your user was an instance of a User that has a prototype function “addTopic(topicID)”, would you create a mutation something like:

mutations.ADD_TOPIC = function(state,{ user, topicID })
{
user.addTopic(topicID);
}


#4

I would usually have a generic mutation, yes - but not that generic usually - I would have one per collection /object type (which could all internally rely on a more generic helper function similar to the one you showed above) - mainly so that I have declarative mutation names.

It becomes unnecessarily distracting when my devtools mutation log is full of GENERIC_PROP_MUTATION and it’s not obvious what piece of state it is working on.

I only keep plain objects in vuex, no class instances or anything like that.


#5

Sorry to resurrect this…but how do you deal with code duplication?

Eg,

computed: {
  users: () => this.$store.getters.userSet,
  topics: () => this.$store.state.topics,
},
methods: {
  userTopics(user) { // this could also be done for the whole user collection in the getter we created above.
    return user.topics.map(topicId => this.topics[topicId]) 
  }
}

It’s pretty easy to justify the need to access a User’s Topics throughout your application, and having to write all that boilerplate just to do that seems awfully tedious, and unmaintainable.


#6

Not sure if this helps you or not, but Vuex getters can return functions :wink:

in a vuex store;

getters: {
  userTopics: (state => (userId) => (
     state.users[userId].topics.map(topicId => state.topics[topicId]) 
   )), ...
},

in a component

{
  computed: {
    mapGetters(['userTopics', ... ])
  },
  methods: {
    getUserTopics(userId) {
      return this.userTopics(userId);
    },
  }

There’s an example in the vuex/getters section;
https://vuex.vuejs.org/en/getters.html


#7

If you are finding yourself writing the exact same code in multiple components it might be useful to create a mixin.


#8

For your 2nd question, did you find any suggestions for child mutating object passing? I could understand the reason we should not pass the object as the payload of mutation. Bug Looking it up in state is too much code duplication to me.


#9

No, I decided to keep it strict and look up the object in the state.


#10

Thank you for replying!

For now, I choose to pass the mutating child object because I am dealing with a 5MB REST return with crazy schema structure. I know it sounds crazy, but that is what we got from the streamset collector.

I will try to adopt normalizr later to see if it help.


#11

But what if the hierarchy is the point of the nested data, not something you can normalise? Like a filesystem tree, or complex (json) schema? I suppose you could flatten a filesystem tree and index it by path, but you might need the hierarchy to build the components, like in the Tree View example.


#12

There are of coure edge cases where every best practice fails …


#13

I am dealing with something very similar to the case here. As long as you know what you are mutating, the getter you are abstracting, it is not lame to pass all the object ref around. So, My 2 cents takeaway I got is, keep the data schema engraved in your head and make friends with vue-devtools.


#14

But how do you make Vuex detect changes if you pass the data around hierarchically via props? Of course the data value changes because these are plain Javascript references, but you actually bypass Vuex, i.e. you don’t actually go through vm.$store.state. Is there a way to link a plain Javascript reference value to a getter or other guard in Vuex?


#15

@LinusBorg - Fantastic help - thanks! However, If I am returning an array from my store/state via a map please can you explain how I extract the “key” for use in the v-for loop i.e. (user, key, index) ? As what I am getting from the store object is an array but without the original ID’s, in your case user: 1234 - here’s what is showing in Vue Dev Tools:
Computed
users:Array[4]
0:Object
1:Object
2:Object
3:Object

But if I pull in the straight object, I get all the useful IDs. which also have the “key” - I’d love to be able to pull in a specific array but I seem to be losing the key during the map exercise.

Thanks!


#16

Well, usually you would also have the id on the object itself as well, not just as the key of the item in the entities collection.

You would usually he these items from an api, right? And those usually come with an id.


#17

HI @LinusBorg - really big thanks for looking at this :smiley:

I’m just extending your example above - I’m actually using it to manage a very complex navigation Object - but going back to your example, so you have a list of users, e.g.:
users: {
1234: {… }, // ids as keys
46473: { name: ‘Tom’, topics: [345, 3456] } // keep only ids of nested items.
},
userList: [46473, 1234] // keep array of the ids as well for easy sorting.

Your computed data set retrieves the array via the store/getter from a mapped array reference to the users, so you receive back an array of Objects from the store/getter (although this “could” be a sub-set, e.g. “coolUsers”:[8820,8821, etc.])…but let’s suppose you want the whole list back for now.

My question though is when that array arrives back, it does not appear to maintain the original user ID that one can use as the key - you get:

Computed
userSet:Object
0:Object
1:Object
2:Object
…etc.

If you pull back the whole data set, e.g.

 userSet(state) {
        return state.users;
    }

then you get back an array that looks like this:

Computed
userSet:Object
1234:Object
2345:Object
3456:Object
…etc.

therefore the v-for="(section, key, index) in userSet" doesn’t work, as the user’s original ID does not appear to be being added to the returned array.?

Hope that makes sense :¬)


#18

I see that I ddi not make myself clear in my previous reply. What I meant to say was:

Usually, all of those objects (i.e. the user objects) should have their id also on the object. and usually, you don’t have ot actually implement that as that’s how you get them from your API, right? your API won’t send you:

{ name: 'Tom' }

it will usually send you:

{ id: '987a4', name: 'Tom' }

(if your API doesn’t give you keys or you track objects you only created locally, create a key yourself)

Then you use that ID in the users object as a key for eas< access:

users: {
  987a4: { id: '987a4', name: 'Tom' }
}

So when you use the getter that puts them back into an array, you still have those ids in the objects themselves:

getters: {
  userSet: (state) => state.userList.map( userId => state.users[userId] )
  /* returns: [ { id: '987a4', name: 'Tom' }, ... ]

  */
}

And therefore, you can easily access them in your templates or components by reading it from the user object:

<li v-for="user in userSet" :key="user.id">
  <a @click="$store.dispatch('deleteUser', user.id)">Delete User</a>
</li>

How to mutate nested array?
#19

@LinusBorg,

<li v-for="user in userSet" :key="user.id">
  <a @click="$store.dispatch('deleteUser', user.id)">Delete User</a>
</li>

why are you construct an array of whole objects when all you need is the object ids? Are there any benefit?


#20

That was just a short example meant to demonstrate another aspect.

Usually you would use the full userSet getter situations where you need an ordered list of users with their full data.