简体   繁体   中英

Vue prop not updating properly in a child component

I've got a child component that receives a prop from the parent component. I've added an event to update the prop in the parent component when a button is clicked. But the child component does not detect the prop change.

Approximate code:

Parent component:

<template>
  <table>
    <Student v-for="(student, index) in students"
      :key="index"
      :student="student"
      :attendance="attendance[student.id]"
      @attended-clicked="changeAttendance"
    ></student>
  </table>
</template>

<script>
  export default {
    data() {
      return {
        students: [
          {id: 1, name: 'Pepito'},
          {id: 2, name: 'Sue'},
        ],
        attendance: {
          1: {attended: true},
          2: {attended: false},
        }
      }
    },
    methods: {
      changeAttendance(studentId) {
        this.attendance[studentId].attended = !this.attendance[studentId].attended;
      }
    }
  }
</script>

Child component:

<template>
  <tr>
    <td>{{ student.name }}</td>
    <td><v-btn :color="attendance.attended ? 'green' : 'red'"
          @click="$emit('attended-clicked', student.id)"
        >Attended</v-btn>
  </tr>
</template>

<script>
  export default {
    props: ['student', 'attendance']
  }
</script>

I've tried to keep the code as minimal as possible to give you an idea of what I'm trying to achieve. The expectation is that when the button is clicked, attendance.attended will change.

I can verify that the value is changing when clicked using Vue developer tools (although I have to press the "force refresh" button to see the change). But apparently the child view is not picking up on the change.

Is there something I'm doing wrong here that's breaking reactivity?

I've also tried using Vue.set with some code like this:

methods: {
  changeAttendance(studentId) {
    let attendance = this.attendance[studentId];
    attendance.attended = !attendance.attended;
    Vue.set(this.attendance, studentId, attendance);
  }
}

No difference.

I appreciate any insight into what I'm doing wrong, thanks!

According to the comments, the underlying problem is that attendance gets set to {} , meaning the attendance[student.id] objects are not really there or not being tracked anymore according to the change detection part in the VueJS docs . This is what should be done by the Vue.set example code provided in the question.

Basically I see two options how to solve this:

  1. Set attendance to always have all students ids available when the API returns a result, making the object always have an already working object
  2. Start tracking the reactivity when you need to get a value from attendance .

Since I don't know about the API and how it's being called, I'll provide an answer for option 2. The provided code almost worked, it only needs two changes to the Parent component to make sure the students attendance object is always filled and it will get tracked in the future:

<template>
  <table>
    <!-- :attendance needs an object to work -->
    <Student
      v-for="(student, index) in students"
      :key="index"
      :student="student"
      :attendance="attendance[student.id] || {}"
      @attended-clicked="changeAttendance"
    />
  </table>
</template>

<script>
import Vue from "vue";
import StudentVue from "./Student.vue";

export default {
  data() {
    return {
      students: [{ id: 1, name: "Pepito" }, { id: 2, name: "Sue" }],
      attendance: {}
    };
  },
  methods: {
    changeAttendance(studentId) {
      // make sure to have an object when accessing a student!
      let attendance = this.attendance[studentId] || {};
      attendance.attended = !attendance.attended;
      // start tracking - this will always "reset" the tracking though!
      Vue.set(this.attendance, studentId, attendance);
    }
  },
  components: { Student: StudentVue }
};
</script>

Since this.attendance[student.id] || {} this.attendance[student.id] || {} is being used multiple times, I'd capsulate this into a getAttendance(studentId) {...} function though. In there, Vue.set could be used to start tracking if the object did not exist before, making changeAttendance a bit simpler and the intention of this || {} || {} more clear.

<template>
  <table>
    <Student
      v-for="(student, index) in students"
      :key="index"
      :student="student"
      :attendance="getAttendance(student.id)"
      @attended-clicked="changeAttendance"
    />
  </table>
</template>

<script>
import Vue from "vue";
import StudentVue from "./Student.vue";

export default {
  data() {
    return {
      students: [{ id: 1, name: "Pepito" }, { id: 2, name: "Sue" }],
      attendance: {}
    };
  },
  methods: {
    changeAttendance(studentId) {
      let attendance = this.getAttendance(studentId);
      attendance.attended = !attendance.attended;
    },
    getAttendance(studentId) {
      const studentAttendance = this.attendance[studentId];
      if (!studentAttendance) {
        Vue.set(this.attendance, studentId, { attended: false });
      }
      return this.attendance[studentId];
    }
  },
  components: { Student: StudentVue }
};
</script>

If the code base is using this.something[something] in other components, the this.getAttendance might look a bit inconsistent. In that case, it's probably better to change the result of the API call to all students with a default for them before feeding it to the Parent component then. Personally, I'd try to use that option. And I'd use this.$set instead of Vue.set to get rid of the import Vue line... ;)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM