Search code examples
sortingvue.jsvuefire

Duplicate Keys in Vue v-for Loop When Table is Sorted


My Vue application brings in data from Firestore using vuefire.

I import the data from a the 'lines' collection as follows:

firestore() {
  return {
    lines: db.collection("lines")
  }
}

Each record has a Firestore generated id, which I then use a a key in a v-for loop like:

<thead>
  <tr>
    <th>Code</th>
    <th @click="sort_string(lines,'name')"> Name</th>
    <th>Quantity</th>
  </tr>
</thead>
<tbody v-for="line in lines" :key="line.id">
  <tr>
    <td>{{line.code}}</td>
    <td>{{line.name}}</td>
    <td>{{line.quantity}}</td>
    <button @click="pick_one(line)">+1</button>
    ...

In have a method pick_onewhich changes the quantity on Firestore directly:

pick_one(line) {

  const new_quantity = line.quantity + 1;      
  db
    .collection("lines")
    .doc(line.id)
    .update({ quantity: new_quantity });
}

All of this works fine until I sort() the underlying array ('lines').

If I sort the table and then call the function pick_one I get a duplicate key error:

[Vue warn]: Duplicate keys detected: 'RaTIINFWTQxHQPyRmfsQ'. This may cause an update error.

I can only assume this has something to do with the way that Vuefire handles update() calls, since the act of sorting the array does not cause this problem, only updating a line in the array while it is sorted.

My sort function (vue method) looks like this:

sort_string(table, column) {
  console.log("sorting")
  this.sort_toggle = -this.sort_toggle;

  return table.sort((a, b) => {
    if (
      a[column].toString().toUpperCase() <
      b[column].toString().toUpperCase()
    ) {
      return -this.sort_toggle;
    }
    if (
      a[column].toString().toUpperCase() >
      b[column].toString().toUpperCase()
    ) {
      return this.sort_toggle;
    }
    return 0;
  });
},

Is there a way to avoid this behaviour?


Solution

  • Phil's comment provided the clue to this behaviour - in that the sort() function is working on the underlying data rather than a copy.

    I have modified my template so that the v-for now loops over a computed array which can be sorted using the (slightly modified) function.

    The sort function now uses slice() to create a copy of the underlying array:

    computed: {
      sorted_lines() {
        return sort_string(lines, this.sort_column) // name of the column/filed to sort by
      }
    

    The sort_string function now looks like this (with addition of slice()

    sort_string(table, column) {
      console.log("sorting")
      //this.sort_toggle = -this.sort_toggle; // moved elsewhere
    
      return table.slice().sort((a, b) => { // slice() then sort()
      if (
        a[column].toString().toUpperCase() <
        b[column].toString().toUpperCase()
      ) {
        return -this.sort_toggle;
      }
      if (
        a[column].toString().toUpperCase() >
        b[column].toString().toUpperCase()
      ) {
        return this.sort_toggle;
      }
      return 0;
    });
    },