Leos Literak
Leos Literak

Reputation: 9474

Vue array computed via parameterized getter not reactive

I have a list a discussion object containing an array of comments and each comment can hold an array of replies. I display the discussion this way:

<div v-for="comment in comments" v-bind:key="comment._id">
  <Comment :itemId="itemId" :comment="comment" />
  <Replies v-if="comment.replies.length > 0" :itemId="itemId" :comment="comment" />
</div>
<Button value="Load more" @clicked="loadMoreComments(itemId)" />

and Replies:

<div v-for="reply in replies" v-bind:key="reply._id">
  <Comment :itemId="itemId" :comment="reply" />
</div>
<Button :value="Load more" @clicked="loadChild()"/>

As you can see both use the same pattern. They differ in a computed property:

computed: {
  comments() {
    return this.$store.getters.DISCUSSION.comments.map(id => this.$store.getters.GET_COMMENT(id));
  },
  replies() {
    return this.$store.getters.GET_REPLIES(this.comment).map(id => this.$store.getters.GET_COMMENT(id));
  },
},

When I hit the Load more button for comments, new comments appear. But when I hit the Load more button in replies, then no new reply is displayed though I can see in debugger that the array was enlarged.

Vuex store submodule:

state: () => ({
  discussion: {
    incomplete: true,
    comments: [],
  },
  comments: {},
}),

getters: {
  DISCUSSION: state => state.discussion,
  GET_COMMENT: state => id => state.comments[id],
  GET_REPLIES: state => (comment) => {
    if (comment.allShown) {
      return comment.replies;
    }
    return comment.replies.slice(0, REPLY_LIMIT);
  },
},
mutations: {
  APPEND_COMMENTS: (state, payload) => {
    const { comments, incomplete, userId } = payload;
    state.discussion.incomplete = incomplete;
    const commentIds = [];
    comments.forEach(comment => processComment(state, comment, commentIds, userId));
    state.discussion.comments = state.discussion.comments.concat(commentIds);
  },
  PREPEND_COMMENTS: (state, payload) => {
    const { comments, userId } = payload;
    const commentIds = [];
    comments.forEach(comment => processComment(state, comment, commentIds, userId));
    state.discussion.comments = commentIds.concat(state.discussion.comments);
  },
  SET_REPLIES: (state, payload) => {
    console.log('SET_REPLIES');
    const { commentId, replies, userId, replace } = payload;
    const comment = state.comments[commentId];
    if (!comment) {
      return;
    }

    state.comments[commentId].showAll = true;
    const commentIds = [];
    replies.forEach(reply => processComment(state, reply, commentIds, userId));
    if (!comment.replies || comment.replies.length === 0 || replace) {
      state.comments[commentId].replies = commentIds;
    } else {
      state.comments[commentId].replies = comment.replies.concat(commentIds);
    }
  },
}

function processComment(state, comment, commentIds, userId) {
  if (comment.replies) {
    const repliesIds = [];
    comment.replies.forEach((reply) => {
      reply.voted = hasVoted(reply.votes, userId);
      state.comments[reply._id] = reply;
      repliesIds.push(reply._id);
    });
    comment.replies = repliesIds;
    comment.allShown = comment.replies.length < REPLY_LIMIT;
  } else if (!comment.parentId) {
    comment.replies = [];
    comment.allShown = false;
  }
  state.comments[comment._id] = comment;
  commentIds.push(comment._id);
}

The complete source code is there: https://github.com/literakl/mezinamiridici/tree/comment_refactoring/spa

Here is minimum reproducible codesandbox: https://codesandbox.io/s/frosty-taussig-v8u4b?file=/src/module.js

I have verified that this happens because of the getter with a parameter. When I put the reply in static array so I could use parameter-less getter, it started to work.

I follow this recommendation: https://forum.vuejs.org/t/vuex-best-practices-for-complex-objects/10143

Where is the issue?

Update:

One thing that smells is the mutation GET_REPLIES because it works on the passed object. So Vue has no chance to detect that the object is from the state. So I have rewritten it to pass only commentId and load the comment from the state, but it did not help.

Upvotes: 1

Views: 633

Answers (1)

Anatoly
Anatoly

Reputation: 22768

I guest you should replace showAll with allShown prop and also use Vue.set where you add new keys to comments object because due to Vue caveats Vuex doesn't see new props, see caveats for objects

    SET_REPLIES: (state, payload) => {
      console.log("SET_REPLIES");
      const { commentId, replies, userId, replace } = payload;
      const comment = state.comments[commentId];
      if (!comment) {
        console.log(`Comment ${commentId} not found`);
        return;
      }

      state.comments[commentId].allShown = true;
      // state.comments[commentId].showAll = true;

...

function processComment(state, comment, commentIds, userId) {
  if (comment.replies) {
    const repliesIds = [];
    comment.replies.forEach(reply => {
      Vue.set(state.comments, reply._id, reply);
      // state.comments[reply._id] = reply;
      repliesIds.push(reply._id);
    });
    comment.replies = repliesIds;
    comment.allShown = comment.replies.length < 3;
  } else if (!comment.parentId) {
    comment.replies = [];
    comment.allShown = false;
  }
  Vue.set(state.comments, comment._id, comment);
  // state.comments[comment._id] = comment;
  commentIds.push(comment._id);
}

Also correct GET_REPLIES call like this:

  computed: {
    replies() {
      return this.$store.getters
        .GET_REPLIES(this.comment) // passing comment itself instead of its id
        .map(id => this.$store.getters.GET_COMMENT(id));
    }
  },

corrected example

Upvotes: 1

Related Questions