Search code examples
vue.jsvuejs3vue-component

Vue3.js why doesn't my passing of a prop work (or raise an error)?


I have a component (a <select> menu) which I need to render more than once.

I set it up to receive a prop to determine which of its options is selected in each case (the first menu should have Perth selected and the second one Melbourne):

App.vue

<template>
  <div>
    <SelectMenu :selectedItem="menuASelectedItem" :options="options" />
    <SelectMenu :selectedItem="menuBSelectedItem" :options="options" />
  </div>
</template>

<script>
import SelectMenu from './SelectMenu.vue';

export default {
  components: {
    SelectMenu
},
  data() {
    return {
      menuASelectedItem: 3, 
      menuBSelectedItem: 2, 
      options: [
        { value: '1', label: 'Sydney' },
        { value: '2', label: 'Melbourne' },
        { value: '3', label: 'Perth' },
      ],
    };
  },
};
</script>

and I assumed I could just use that value in the component:

SelectMenu.vue

<template>
  <select v-model="SelectedItem">
    <option v-for="option in options" :key="option.value" :value="option.value">
      {{ option.label }}
    </option>
  </select>
</template>

<script>
export default {
  props: {
    selectedItem: {
      type: String,
      required: true,
    },
    options: {
      type: Array,
      required: true,
    },
  },
};
</script>

But this doesn't work. Or generate an error. It just doesn't select anything in the menus. By Googling I have figured out that I can do this by adjusting the component so that the prop becomes a local variable:

SelectMenu.vue fixed:

<template>
  <select v-model="localSelectedItem"> <!-- localSelectedItem, not SelectedItem -->
    <option v-for="option in options" :key="option.value" :value="option.value">
      {{ option.label }}
    </option>
  </select>
</template>

<script>
export default {
  props: {
    selectedItem: {
      type: String,
      required: true,
    },
    options: {
      type: Array,
      required: true,
    },
  },
  data() { // data block handles turning selectedItem into localSelectedItem
    return {
      localSelectedItem: this.selectedItem,
    };
  },
};
</script>

So, I've got my code working, but I need to understand why it didn't work the first, more obvious way. Isn't that exactly what props are for? The prop is a variable sent into the component from outside. Why can't it be used directly?


Solution

  • You shouldn't mutate the props. In this case, v-model enables two-way binding and mutates the prop. To avoid this, you could bind it to a writable computed property that retrieves the prop value. When it is set, emit the value to the parent:

    <template>
      <select v-model="localeSelectedItem">
        <option v-for="option in options" :key="option.value" :value="option.value">
          {{ option.label }}
        </option>
      </select>
    </template>
    
    <script>
    export default {
      props: {
        selectedItem: {
          type: String,
          required: true,
        },
        options: {
          type: Array,
          required: true,
        },
      },
      emits: ["update:selectedItem"],
      computed: {
        localeSelectedItem: {
          get() {
            return this.selectedItem;
          },
          set(value) {
            this.$emit("update:selectedItem", value);
          },
        },
      },
    };
    </script>
    
    

    and in the parent bind it as follows :

    <template>
      <div>
        <SelectMenu v-model:selectedItem="menuASelectedItem" :options="options" />
        <SelectMenu v-model:selectedItem="menuBSelectedItem" :options="options" />
      </div>
    </template>
    

    You could learn more about this feature in component v-model