Vuex best practices for complex objects


#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.


#21

If I use full userSet in my vue components, this means that I spread the domain logic across these components. What about that concern? In this case I can put my domain logic in the object itself, but how I understand this is no best practice. Why?


#22

I talk about this snippet:

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]) 
    }
}

I think getting user topics is the user object responsibility, not the component, that use the user object. As an option, I can put all my domain logic in the store, then I need no full userSet in my components, but only the user ids. Where am I wrong?


#23

Hi, as I understand, you should, where possible keep all the logic in the “getter” / store side, and within the component, just “go get” the data you want from the getter.

You can have as many “getters” as you want, and this is where mapping comes in handy, as you can just call whichever one you need inside your component, so this has the added benefit of centralising your logic and then being able to call it in/from/across multiple components.


#24

Thanks for this best practice for complex object.

In my project, I have a data structure very similar to the example proposed in this discussion.

Let’s say I have a user-list component with the following template:

<div>
  <div v-for="userId in userList" :key="userId">
    <user :user-id="userId"/>
  </div>
</div>

The user component only displays the user name

<div>
  {{userName}}
</div>

And is defined as follows:

props: ['userId'],
computed: {
   ...mapGetters(['userById']),
  userName: function () {
    return this.userById(this.userId).name;
  }
},
updated: function() {
  console.log(`user ${this.userId} updated`);
}

Finally, the getter is implemented as follows:

userById = (state) => (userId) => {
  return state.users[userId];
};

And a mutation to add a user as:

state.userList.splice(0, 0, userId);
Vue.set(state.users, userId, newUser);

Problem: when a user is added with the mutation defined above, then all the user components in the list are updated. Why?

If I change the mutation to:

state.userList.splice(0, 0, userId);
state.users[userId] = newUser; // instead of Vue.set(state.users, userId, newUser)

Then, I can see the new added user at the head of the list, and all the other users in the list are not updated anymore. Good. But, if I later update that new user with the following mutation:

Vue.set(state.users, userId, updatedUser);

Then the corresponding user component in the list is not updated.

How I can implement the following desired behavior:

  • adding a user should not cause all the other users to be updated
  • updating an existing user should update the corresponding user component in the view

#25

Thats ok for saving only. But what if we need to watch these data? Every time if we change just one of the users object property, the watchers will fire in every place where we watch ‘users.’ Is any solution in Vue for watching nested data only? Additional conditions in watchers added only for blocking of execution of the watcher code like “if (oldAnswer != newAnswer) …” are so ugly…


#26

The reactivity system has certain limitations as long as we can’t use Proxies (which we will in the upcoming 2.*-next branch).

I’m not sure I understand your described scenario in detail, but adding a new object property or pushing to an array would both trigger all watchers a accessing this array/object, so the implications would be similar.


If component's computed property depends on element in array, will any change (e.g. push) to that array trigger update in all component instances?
#27

If an user object contains a lot of deep structures (arrays, objects), so then if we have really a lot of users, related getter (with huge amount of map() etc.) can be very slow, can’t be? Probably, the UI will be “frozen”.

I assume that getters are only synchronous and they are “recalculated” after each change of whatever piece of state, which belongs to those nested structures of users objects.

If we create an action to make it async, then we lose the reactivity and we will have to call the update action ourselves every time.

Only solution which come to my mind is a getter, which will return promise and in every component, where we use the getter, we will also have an watch, which will register a then() and process results.


#30

No. if the nested strctures are not referenced in the getter, changing them will not trigger an update of the getter.


#31

OK, so Vuex getters are not deep. But still, if we add an item to the root level array and the getter will be started … if it has to process really a lot of .map() functions to get an array of complete objects (with all nested structures), it will be very slow and since getter is a sync procedure, UI will be “frozen”?


#32

That can happen, but that would be a problem with a getter, a ccomputed property or even more so with a simple component method.

If your data processing is complex and your data set is large enough, it will be blocking anywhere (unless you can move it to a worker or something )


#33

In my opinion, best way to deal with complex mutations is to use spread operator just like this

return { ...state, visibilityFilter: action.filter }