Getter in Vuex doesn't update Vue components but the data is available


#1

I think I have a reactivity issues with Vue (and Vuex). There’s a complex object that is dynamically grouped by date. The content of the object is another array with objects. The structure is shown below:

Structure of my object

    const appointment = {
        '2019-03-05': [
            { id: 1, title: 'Appointment 1', startDate: '2019-03-05 00:00', endDate: '2019-03-06 00:00', date: '2019-03-05 00:00'},
            { id: 2, title: 'Appointment 2', startDate: '2019-03-05 10:00', endDate: '2019-03-05 11:00', date: '2019-03-05 00:00'},
        ],
        '2019-03-06': [
            { id: 1, title: 'Appointment 1', startDate: '2019-03-05 10:00', endDate: '2019-03-05 11:00', date: '2019-03-06 00:00' },
        ]
    };

What I am trying to achieve
I’m building a calendar component where you can add appointments. This is stored in a state property in Vuex. I am using a getter to filter the results into the object defined above. I need this structure to easily group my appointments in a list and render it to the screen. I have no problems with deleting, adding and updating a single record. But as you can see in the object it is possible to create an appointment which lasts more than one day. If that happens I actually copy the appointment, change the correct date and add them to the list. This all happens in the getter from Vuex.

The problem
When updating an appointment it automatically fires the getter function in Vuex. When debugging the generated list it even shows that all the objects (that I have copied) are successfully changed. Only it does not update my Vue. I have tried using Vue.set or even the spread operator. Does anyone know how to fix this issue?

enter image description here

The code (cleaned)

export default {
    namespaced: true,
    state: {
        items: [],
    },
    getters: {
        appointmentsByDate(state) {
            const appointments = _.chain(state.items)
            .filter(a => state.selectedDate.isSame(a.startDate, 'month') || state.selectedDate.isSame(a.endDate, 'month'))
            .map(result => Object.assign({}, result, {
                date: new Date(result.startDate),
                startDate: new Date(result.startDate),
                endDate: new Date(result.endDate),
                groupBy: moment(result.startDate).format('YYYY-MM-DD'),
                entireDay: moment(result.startDate).format('HH:mm:ss') === moment(result.endDate).format('HH:mm:ss'),
                startTime: moment(result.startDate).format('HH:mm'),
                endTime: moment(result.endDate).format('HH:mm'),
            }))
            // This is where I check if there needs to be a copy of the date
            .tap((list) => {
                list.forEach((item) => {
                const copies = Math.abs(moment(item.startDate).diff(item.endDate, 'days'));
                if (copies === 0) return;

                for (let day = 0; day < copies; day += 1) {
                    // Increment day
                    const date = moment(item.startDate).add(day + 1, 'day');

                    // Check if the date is still in the same month
                    if (date.isSame(state.selectedDate, 'month')) {
                    // Copy appointment with the new date
                    const copy = Object.assign({}, item, {
                        date,
                        groupBy: date.format('YYYY-MM-DD'),
                    });

                    // Vue.set(list, list.length, copy);
                    list.push(copy);
                    }
                }
                });
            })
            .filter(a => state.selectedDate.isSame(a.date, 'month'))
            .sortBy(['startDate', 'title'])
            .groupBy('groupBy')
            .value();

            console.log(appointments); // Shows the correct object. It just doesn't update the view.
            return appointments;
        },
    },
    mutations: {
        setAppointments(state, appointments) {
            state.items = appointments;
        },
        addAppointment(state, appointment) {
            // Vue.set(state.items, state.items.length, appointment);
            state.items.push(appointment);
        },
        updateAppointment(state, appointment) {
            /*
            const index = state.items.findIndex(a => a.id === appointment.id);
            Vue.set(state.items, index, appointment);
            */
            state.items = [
            ...state.items.filter(element => element.id !== appointment.id),
            appointment,
            ];
        },
    },
    actions: {
        getAppointments({ commit, rootState }, date) {
            commit('setPrecendingDate', moment(date).add(-RANGE, 'months'));
            commit('setRecentDate', moment(date).add(RANGE, 'months'));

            return fetch(`${rootState.$appUrl}/appointment?date=${date}&range=${RANGE}`)
            .then(response => response.json())
            .then((appointments) => {
                commit('setAppointments', appointments);
            });
        },
        updateAppointment({ commit, rootState }, { id, appointment }) {
            return fetch(`${rootState.$appUrl}/appointment/${id}`, {
            method: 'PUT',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(appointment),
            })
            .then(() => {
                commit('updateAppointment', appointment);
                return appointment;
            });
        },
    },
};

The component that uses the getter:

    <template>
      <div class="appointments">
        <transition-group name="fade" @after-enter="afterEnter">
          <div
            class="appointment-list"
            v-for="(list, key, index) in appointments_"
            :id="`appointment_${generateKey(key)}`"
            :key="generateKey(key)"
            :class="getClassObject(key)"
            :data-index="index"
          >
            <div class="appointment-list__time">
              {{ formatDate(key) }}
            </div>
            <ul class="appointment-list__items">
              <transition-group name="fade">
                <li v-for="(appointment) in list" v-bind:key="appointment.id">
                  <AppointmentItem :data="appointment" />
                </li>
              </transition-group>
            </ul>
          </div>
        </transition-group>
      </div>
    </template>
    
    <script>
    import { mapState, mapGetters } from 'vuex';
    import AppointmentItem from '@/components/appointment/AppointmentItem.vue';
    
    export default {
      name: 'AppointmentList',
      components: {
        AppointmentItem,
        MessageBox,
      },
      data() {
      },
      methods: {
        generateKey(key) {
            return moment(key).format('YYYYMMDD');
        },
      },
      mounted() {
        this.$store.dispatch('appointments/getAppointments', this.selectedDate_.format('YYYY-MM-DD'))
          .catch(() => { this.error = 'Er is iets misgegaan met het ophalen van afspraken. Probeer het later opnieuw.'; })
          .finally(() => { this.loading = false; });
      },
      computed: {
        ...mapGetters({
          appointments_: 'appointments/appointmentsByDate',
        }),
      },
    };
    </script>

Vue doesn't update view when array of objects get changed
#2

I have found the issue. I am copying the object and the id is the same. I’m using a hash as key to fix my issue. Problem solved! :slight_smile: