Why does my two separate Vue components override each others data?

Viewed 515

Use case

Given the following vue component:

<template>
  <div>
    <slot>{{ title }}</slot>
    <ul>
      <li v-for="label in labels" :key="label">
        <input
          type="checkbox"
          v-model="checked"
          :label="label"
          :id="label"
          :value="label"
        />
        <label :for="label">{{ label }}</label>
      </li>
    </ul>
  </div>
</template>

<script>
import Component from "vue-class-component";
import Vue from "vue";

@Component({
  props: {
    labels: {
      type: Array,
    },
    title: {
      type: String,
    },
  },
  watch: {
    checked: [
      {
        handler: "updateParent",
      },
    ],
  },
})
export default class CheckboxList extends Vue {
  checked = [];

  updateParent() {
    this.$emit("update", this.checked);
  }
}
</script>

The component will render a checkbox list of the labels prop it receives from the parent.

When two components are used within the same page, using a v-if toggle as follows:

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png" width="10%" />
    <hr />
    <button type="button" @click="state = 'numbers'">
      Show number checklist
    </button>
    <button type="button" @click="state = 'letters'">
      Show letter checklist
    </button>

    <CheckboxList
      v-if="state === 'numbers'"
      title="numbers"
      :labels="numbers"
      @update="checkedNumbers = $event"
    ></CheckboxList>
    <CheckboxList
      v-if="state === 'letters'"
      title="letters"
      :labels="letters"
      @update="checkedLetters = $event"
    ></CheckboxList>

    <div>
      <span>Checked letters:</span> {{ checkedLetters }}
      <span>Checked numbers:</span> {{ checkedNumbers }}
    </div>
  </div>
</template>

<script>
import Vue from "vue";
import HelloWorld from "./components/HelloWorld";
import CheckboxList from "./CheckboxList.vue";
import Component from "vue-class-component";

@Component({
  components: {
    CheckboxList,
  },
})
export default class App extends Vue {
  numbers = [1, 2, 3, 4, 5];
  letters = ["a", "b", "c", "d"];
  state = "numbers";

  checkedLetters = [];
  checkedNumbers = [];
}
</script>

This will create the following UI:

A checkbox list

Why when toggling between the two components (using v-if) and checking the boxes - the data will mix up resulting the following behavior? enter image description here

A Full working example can be found here: https://codesandbox.io/s/epic-resonance-plwvi?file=/src/App.vue

1 Answers

As part of Vues internal optimizations, it will strive to reuse components within its directives (i.e v-if or v-for for instance) as the default behavior.

This is documented in their docs.

Since unfortunately it's indeed what Vue will default to, consider adding a key prop with unique values in order to 'force' Vue to treat these as separate components:

 <CheckboxList
  key="numbers" <!-- Add this to distinct between the two -->
  v-if="state === 'numbers'"
  title="numbers"
  :labels="numbers"
  @update="checkedNumbers = $event"
  ></CheckboxList>
  <CheckboxList
   key="letters" <!-- Add this to distinct between the two -->
   v-if="state === 'letters'"
   title="letters"
   :labels="letters"
   @update="checkedLetters = $event"
   ></CheckboxList>

Note that this isn't required when components are rendered in one go - without conditional switches or looping:

<!-- 
     Since both are rendered at the same time, 
     they will be considered separate components 
     and won't require a 'key' prop to distinguish between the two 
-->
 <CheckboxList
  title="numbers"
  :labels="numbers"
  @update="checkedNumbers = $event"
  ></CheckboxList>
  <CheckboxList
   title="letters"
   :labels="letters"
   @update="checkedLetters = $event"
   ></CheckboxList>
Related