Child UI not updating in Vue3

71 views Asked by At

I'm having a hard time with using v-model in a chained fashion.

Essentially I have a Parent component that can have many Child instances. Child works fine alone and I want Parent to keep track of all values of Child and spit out an array of the values with some custom formatting.

It kind of works, but removing a child has some weird behaviour.

enter image description here

When I remove the first entry, the data is the expected ['']. However visually I see the wrong child being removed. Any ideas what is going on?

enter image description here


Child.vue

<script setup>
import { ref, watch, defineProps } from 'vue';
const { modelValue } = defineProps(['modelValue']);
const emitUpdate = defineEmits(['update:modelValue']);
const category = ref(0);
const foods = ref(0);

// Emit new value when user picks a new food
watch(foods, () => {
    emitUpdate('update:modelValue', `${category.value} ${foods.value}`);
});
</script>

<template>
    <div>
        <select v-model="category">
            <option value="Fruits">Fruits</option>
            <option value="Pasta">Pasta</option>
        </select>
        <select v-model="foods">
            <option value="oranges">Oranges</option>
            <option value="lasagna">Lasagna</option>
        </select>
    </div>
</template>

Parent.vue

<script setup>
import Child from './Child.vue';
import { ref, defineProps } from 'vue';
const props = defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);

const children = ref([]);

function addChild() {
    children.value.push({ value: '' });
}

function removeChild(index) {
    children.value.splice(index, 1);
    emit('update:modelValue', children.value.map(child => child.value));
}

function updateChildValue(index, value) {
    children.value[index].value = value;
    emit('update:modelValue', children.value.map(child => child.value));
}
</script>

<template>
    <div>
        <button @click="addChild">Add child</button>
        <div v-for="(child, index) in children" :key="index">
            <Child v-model="child.value" @update:modelValue="updateChildValue(index, $event)" />
            <button @click="removeChild(index)">Remove child</button>
        </div>
    </div>
</template>

Usage

import Parent from '@/components/Parent.vue';
const myParentValue = ref('') 

<template>
        ...
        <p>PARENT</p>
        <Parent v-model="myParentValue"></Parent>
        <p>myParentValue: {{ myParentValue }}</p>
        ...
</template>
...

1

There are 1 answers

0
yoduh On BEST ANSWER

Do not use index as your v-for key. The key should be a unique value. When you have an array where elements are being added and removed, the item at each index can change, meaning the index is not a unique identifier for the given item. This is the reason the UI is not updating correctly. Vue is not able to properly diff the changes to the array based on the key.

I suggest adding a unique id field when adding a new child. This is one possible simple way of doing it:

let count = 0
function addChild() {
    children.value.push({ id: count++, value: '' });
}

Then just update your v-for:

<div v-for="(child, index) in children" :key="child.id">

Two other non-critical issues spotted:

First, in Child.vue:

const { modelValue } = defineProps(['modelValue']);

You shouldn't destructure defineProps. You can lose reactivity this way. Always opt to assign the return value of defineProps to a variable, e.g. const props = defineProps. This isn't an issue in your code at the moment because you don't actually use modelValue in your Child component... You can remove this prop and the v-model on the <Child> element in the parent if you want to since they're not technically doing anything. What's really doing all the work is the emits.

Second, and not really an issue, but you don't need to import defineProps, it is automatically available inside script setup

All corrections can be seen in this Vue Playground example