Gecko29
Gecko29

Reputation: 199

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

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
},

Upvotes: 16

Views: 17002

Answers (2)

Phil
Phil

Reputation: 164766

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.

Upvotes: 24

Max S.
Max S.

Reputation: 1461

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>

Upvotes: 0

Related Questions