4

Here's a part of my Vue template:

<ul>
   <li v-for="friend in user.friends">
       <span v-if="checkIfNew(friend.id)"> ... </span>
   </li>
</ul>

Basically, friends is an array of objects, and I want to display the span element, if we have new messages from any of them. That's what checkIfNew() does. It checks whether the friend's id is in the unreadIds array (it contains ids of friends, who sent a message)

This array is being updated in a different method, but, here's the problem: v-if doesn't react to the changes.

Here's a part of the script section:

data(){
   return {
      unreadIds: []
   }
},
methods:{
   checkIfNew(id){
      if(id in this.unreadIds) return true
      else return false
   }
},
computed:{
    user(){
      return this.$store.getters.user;
    }
}

Does anyone have any ideas what am I doing wrong?

Koanna
  • 53
  • 1
  • 1
  • 6
  • Show us how you update `unreadIds`. – Ohgodwhy Jan 07 '20 at 18:41
  • @Ohgodwhy it's in the apollo $subscribe part, in the result: this.unreadIds.push(...). I know the array itself is updating correctly. – Koanna Jan 07 '20 at 18:46
  • 1
    When you say "v-if doesn't react to the changes.", that's not necessary since it's inside the loop. What you need is for the loop to react to the change, which means make a computed, something like `friends() { return this.user.friends }` and base the loop on that. – Richard Matsen Jan 07 '20 at 18:57
  • @eric99 thanks x2 unreadIds are ids of friends, who sent a new message. I'll edit the question, it really is confusing – Koanna Jan 07 '20 at 19:19
  • @RichardMatsen Messages don't affect friends. So when the new message arrives, friends don't change. So the loop is based on friends, but I want it to also react to unreadIds... – Koanna Jan 07 '20 at 19:26

3 Answers3

5

id in this.unreadIds doesn't do what you think it does. See the docs for the in operator. It will return true if the object has the value as a property. So if this.unreadIds had 3 items and you had an id of 1, then the in operator will return true because 1 is a property of the array (this.unreadIds[1] exists).

Instead, you should use includes.

Try rewriting your method like this:

checkIfNew(id) {
  return this.unreadIds.includes(id);
}

Here's a working version of the component that updates the list without the Vuex store code:

<ul>
  <li v-for="friend in user.friends" :key="friend.ids">
    <span v-if="checkIfNew(friend.id)">{{ friend.name }}</span>
  </li>
</ul>
export default {
  data() {
    return {
      unreadIds: [5],
      user: {
        friends: [
          { id: 1, name: 'joe' },
          { id: 5, name: 'nancy' },
          { id: 9, name: 'sue' },
        ],
      },
    };
  },
  created() {
    setTimeout(() => this.unreadIds.push(9), 2000);
  },
  methods: {
    checkIfNew(id) {
      return this.unreadIds.includes(id);
    },
  },
};

Just to prove here that David was correct all along, I put this code in a runable snippet, and cannot find any fault...

Vue.config.productionTip = false
new Vue({
  el: '#app',
  data() {
    return {
      unreadIds: [],
    };
  },
  created() {
    setTimeout(() => this.unreadIds.push(9), 2000);
  },
  methods: {
    checkIfNew(id) {
//      if(id in this.unreadIds) return true
//      else return false
      return this.unreadIds.includes(id);
    },
  },
  computed: {
    user(){
      return {
        friends: [
          { id: 1, name: 'joe' },
          { id: 5, name: 'nancy' },
          { id: 9, name: 'sue' }
        ]
      }
    }
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.js"></script>

<div id="app">
  <ul>
    <li v-for="friend in user.friends" >
      <span v-if="checkIfNew(friend.id)">{{ friend.name }}</span>
    </li>
  </ul>
</div>

The sample above is a bit closer to the original question

  • user is a computed not data
  • unreadIds is initially empty

Upvote from me!

David Weldon
  • 63,632
  • 11
  • 148
  • 146
  • Comments are not for extended discussion; this conversation has been [moved to chat](https://chat.stackoverflow.com/rooms/205613/discussion-on-answer-by-david-weldon-vue-v-if-not-updating). – Samuel Liew Jan 08 '20 at 15:40
5

You want to leverage Vue's reactivity system as the previous answers do not.. they will eventually open you up to inexplicable problems that aren't easily debuggable.

Rather than invoking a method in a v-for (which I guarantee will become problematic for you in the future), you should declare a computed list that contains (or does not contain) the items you want rendered, something like this:

data(){
   return {
      unreadIds: []
   }
},
computed:{
    user(){
      return this.$store.getters.user;
    },
    NewFriends() {
       return this.user.friends.filter(friend => this.unreadIds.includes(friend.id));
    }
}

Your markup would then just be:

<ul>
   <li v-for="friend in NewFriends">
       <span > ... </span>
   </li>
</ul>

And Vue's reactivity system would handle any changes to data dependencies for NewFriends.

You don't want to ever use method calls in a template because method calls are only guaranteed to be invoked once (the same applies to functions that come from computed methods...).

If you find yourself trying to trigger re-renders and dependency checks manually, you will want to re-think your design.

ETA: The only time you will ever want to invoke a function of any kind in a template is to respond to events.

gabriel.hayes
  • 2,267
  • 12
  • 15
  • 1
    +1 This is the best practise compared to the other answers! The official [style guide](https://vuejs.org/v2/guide/list.html#v-for-with-v-if) also recommends this. – kano Jul 24 '20 at 09:42
1

I'll have a stab at it - someone posted this earlier and deleted it. You need checkIfNew() to be a computed not a method for reactivity in the template.

Since you need to pass in the id, the computed needs to return a function.

data(){
   return {
      unreadIds: []
   }
},
computed:{
    user(){
      return this.$store.getters.user;
    },
   checkIfNew(){
      return (id) => {
        return this.unreadIds.includes(id);
      }
   }
}

As David Weldon says, you should ideally change the array immutably - and probably why ohgodwhy asked the original question.

  • Is the fake line for dependency check even necessary? – gabriel.hayes Jan 07 '20 at 19:51
  • @user1538301 ... actually, it's not – Koanna Jan 07 '20 at 19:53
  • It seems a bit hacky. – gabriel.hayes Jan 07 '20 at 19:53
  • just making checkIfNew() a computed fixed it – Koanna Jan 07 '20 at 19:55
  • @Koanna @eric99 what happens if `this.unreadIds` or `user.friends` is changed? Also, this will still show empty bullet points for friends that aren't "new" – gabriel.hayes Jan 07 '20 at 20:10
  • @eric99 As discussed [here](https://stackoverflow.com/questions/59636004), methods do establish a reactive dependency, which should re-render the template. For that reason, I'm unclear why moving this to a computed property would suddenly fix the issue for the OP. Weird. – David Weldon Jan 07 '20 at 22:43
  • Interesting, I always thought methods were not reactive - but my first answer was incorrect anyway (see pre my last edit) - will take a look at the link. It's possible Vue reactivity has evolved since my last project, those guys are pretty smart. –  Jan 07 '20 at 23:15
  • This is simply __bad__ solution and __should not be an accepted answer__. Real fix is really only changing `id in this.unreadIds` to `includes(id)`. You are using computed property to create new function that will be called as if it is normal `method`. Main advantage of computed properties is value caching (body is not executed if not needed and cached value is returned instead). What is cached here is a function itself so using `computed` adds no value, only one more level of indirection. It's harder to read and understand and it will be slower than using method directly.... – Michal Levý Jan 08 '20 at 11:25
  • @MichalLevý I changed `in` to `includes`, but that wasn't enough – Koanna Jan 08 '20 at 14:05
  • @Koanna Well then there was some additional problem. Doesn't change anything about the fact that using computed in a way outlined in this answer is nonsense. Take a look at David's answer and example. It's using method andt just works... – Michal Levý Jan 08 '20 at 14:19
  • 4
    According to this [post](https://forum.vuejs.org/t/can-computed-property-be-a-function-which-itself-return-another-function/7572) by Linus Borg (Vue contibuter) computed properties that return a function are ok. There seems to be a lot of unproven statements around what should and should not be done, but keep in mind that Vue is designed to be open and flexible rather than opinionated. – Richard Matsen Jan 08 '20 at 23:37