Getter not reactive when updating the property of an object in Vuex
Asked Answered
B

2

16

I am experiencing a few issues regarding reactivity in my application.

I have a Vuex store where I have an object 'songs' containing 'song' objects where the key is their id. For each song object within, I have various properties, with the one in question being 'playing' a boolean value.

e.g.

songs = {
   2: {playing: false},
   7: {playing: true)
}

Within my application I have a list, using v-for for all these songs with a button that toggles the playing property of the song. When the button is pressed I dispatch the action I have created passing in the songid associated with the button that has been pressed

this.$store.dispatch('toggleSongPlaying', {songid: songid})

Depending on whether the song is playing or not, I wish to display the appropriate icon.

My simplified vuex store for my issue consists of the following:

const state = {
  songs: {}
};

const mutations = {
  update_song_playing(state, payload) {
     var songObj = state.songs[payload.songid]
     var playingBool = !songObj.playing
     Vue.set(songObj, 'playing', playingBool)
  }
};

const actions = {
  toggleSongPlaying({ commit }, payload) {
     commit("update_song_playing", payload)
  }
};

const getters = {
  getSongs: state => state.songs
};

The setting functionality is working fine, the issue is with reactivity. When the toggle button is pressed, the icon change does not occur. Triggering a rerender does however result in the icon changing. Within my component, I am using mapGetters like so:

computed: {
    ...mapGetters({
      songs: "getSongs"
    })
}

Is there something I am missing in order to get reactivity to work correctly with this nested object?

Thanks for any suggestions!

EDIT - Template code for list and icons

<v-list dense>
   <v-subheader>Songs List</v-subheader>
   <v-list-item-group v-model="activeSong" color="primary">
         <v-list-item
              v-for="(song, songID) in songs"
              :key="songID"
         >
               <v-icon class="mr-3" @click.stop="toggleSongPlaying(songID)">{{song.playing ? 'mdi-pause' : 'mdi-play'}}</v-icon>

                // song details etc...
                <v-list-item-content>
                    <v-list-item-title>
                    </v-list-item-title>
                </v-list-item-content>
                    
          </v-list-item>
   </v-list-item-group>
</v-list>
methods: {
   toggleSongPlaying(songid) {
       this.$store.dispatch('toggleSongPlaying', {songid: songid})
   }
},

EDIT2 In a separate component in created, I populate the songs object with a song like so

created() {
    var songid = "12"
    var song = {
        length: ...,
        playing: false,
    }

    var payload = {
        song: song,
        songid: songid
    }

    this.$store.dispatch('addSong', payload)
},

Within Vuex

Action:

addSong({ commit }, payload) {
   commit("add_song", payload);
}

Mutator:

add_song(state, payload) {
    state.songs[payload.songid] = payload.song
},
Burley answered 8/9, 2020 at 1:0 Comment(6)
Within the component, it calls the toggleSongPlaying action.Burley
I've just added it at the bottom. When I press the icon to toggle the boolean variable no change occurs. But if a rerender occurs such as clicking on an item in the list, the icon changesBurley
I've also tried this in the Vuetify sandbox and cannot reproduce the problemConstantan
Hmm, that's strange. For simplicity of my question, I have said that the state songs is initialised with 2 song objects. In practice, I am setting songs in the created hook with a setter taking in the song id and an object consisting of various props including the playing property. Could that be the issue?Burley
Please show how you are assigning song objects to your storeConstantan
Ahh of course, I needed Vue.set in my addSong method!Burley
C
24

The way you are assigning songs to your state is not reactive

For Objects

Vue cannot detect property addition or deletion

Change your add_song mutation to replace the songs state property with a new one, including the new song. This treats songs as immutable

add_song: (state, { songid, song }) => {
  state.songs = {
    ...state.songs,
    [ songid ]: { ...song } // break any object references, thank me later
  }
},

Now you don't need to use Vue.set because the payload property has been added reactively. Your update_song_playing can simply be

update_song_playing: (state, { songid }) => {
  const song = state.songs[songid];
  if (song) {
    song.playing = !song.playing;
  }
}

You can also use Vue.set in your add_song mutation but I've always felt Flux-based stores work best with immutable data.

Constantan answered 8/9, 2020 at 2:2 Comment(1)
Just in case, Object.assign(state.songs, ...) did not work but the equating. Some might find lodash.com/docs#cloneDeep interesting, too. Thank you very much! Purely genius! Purely awesome! ✨Uric
G
0

Call $forceUpdate() after calling your dispatch such as:

<v-icon class="mr-3" @click.stop="toggleSongPlaying(songID), $forceUpdate()">{{song.playing ? 'mdi-pause' : 'mdi-play'}}</v-icon>
Gingerly answered 13/5, 2022 at 13:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.