41

I have a reusable component that is a video.js video player. This component works fine when the data is passed in on the initial DOM load.

I need to figure out why my component is not re-rendering after the state is updated in Vuex.

The parent component passes down the data for the video via props. I also have this set to be used with multiple videos and it works fine with a single one or many.

<div v-for="video in videos" :key="video.id">
  <video-player :videoName="video.videoName" :videoURL="video.videoURL" :thumbnail="video.thumbnail"></video-player>
</div>

I'm setting the initial state to a generic video for all users in my Vuex store.

getFreeVideo: [
  {
    videoName: "the_video_name",
    videoURL:  "https://demo-video-url.mp4",
    thumbnail: "https://s3.amazonaws.com/../demo-video-poster.jpg"
  }
]

This is set in data in videos (and later set to getFreeVideo)

 data () {
   return {
     videos: []
   }
 }

I'm setting videos in data() to getFreeVideo in the store within the created() lifecycle:

    this.videos = this.getFreeVideo

..and checking if a user has a personal video and updating the state in the created() lifecycle.

 this.$store.dispatch('getFreeVideo', 'the_video_name')

This makes a request with axios and returns our video data successfully.

I'm using mapState import { mapState } from 'vuex to watch for a state change.

 computed: {
  ...mapState(['getFreeVideo'])
}

I am not seeing why this.videos is not being updated. Here, my assumption regarding the expected behaviour would be videos[] being updated from the state change and a re-rendering of the component.

As you can see below, the state has been updated, and the videoUpdate() within the computed properties has the new data as well:

vuex Vuex ..but, videos[] is never updated thus the video component never gets the props new props etc..

A couple of notes:

  • already tried, hiding the child component with v-if (and showing after state change)
  • tried setTimeout to test things, but the data will come through and then the videoJS player never instantiates correctly (must have initial data)
  • tried doing this with a local method / not using Vuex state
  • console is showing error TypeError: Cannot read property '_withTask' of undefined but this happens even when the demo video loads correctly, so this seem unrelated, and I can't find anything anywhere in here that presents itself as undefined.

TL;DR

I basically can't get child component to re-render after the state change. And although I can get the data into videos[] with a different structure, it still never re-renders.

Why is the data not making it through, and the re-render never happening?

Please don't post answers that only contain links to 'understanding reactivity' or something without any explanation.

appended for @acdcjunior

//action   
getFreeVideo: (context, videoName) => {
    axios({
      method: 'post',
      url: 'https://hidden-for-posting',
      data: {
        action: 'getVideo',
        userId: '1777', // (hardcoded to test)
        videoName: videoName
      },
      headers: {
        'x-api-key': apiKey,
        'Content-Type': 'application/json'
      }
    })
    .then(response => {
      let video = [
        {
          videoName: response.data.videoName,
          videoURL: response.data.videoURLs.mp4,
          thumbnail: response.data.thumbnails['1280']
        }
      ]
      return context.commit('updateGetFreeVideo', video)
    })
    .catch(error => {
      if (error.response) {
        console.log(error.response)
      } else if (error.request) {
        console.log(error.request)
      } else {
        console.log('Error', error.message)
      }
      console.log(error.config)
    })
}

// mutation:
updateGetFreeVideo: (state, payload) => {
  return state.getFreeVideo = payload
}

// getter:
getFreeVideo: state => {
  return state.getFreeVideo
}
ansidev
  • 361
  • 1
  • 3
  • 17
Jordan
  • 1,650
  • 2
  • 18
  • 17

3 Answers3

55

NOTE: at the bottom of this answer, see the general point I make about update/reactivity issues with Vue.


Now, about the question, based on the code you posted, considering the template:

<div v-for="video in videos" :key="video.id">

It picks the videos from:

 data () {
   return {
     videos: freeVideo
   }
 }

Although it initializes from freeVideo, in nowhere in your code you show an update of videos.

Solution:

You already have the state mapped in the getFreeVideo computed:

computed: {
  ...mapState(['getFreeVideo'])
}

Use it:

<div v-for="video in getFreeVideo" :key="video.id">

Update:

I'm setting videos in data() to getFreeVideo in the store within the created() lifecycle:

    this.videos = this.getFreeVideo

This is not enough to keep this.videos updated with whatever this.getFreeVideo is. Whenever something is set to this.getFreeVideo it will only change this.getFreeVideo, not this.videos.

If you want to automatically update this.videos whenever this.getFreeVideo changes, create a watcher:

watch: {
  getFreeVideo() {
    this.videos = this.getFreeVideo
  }
}

And then keep using videos in the v-for:

<div v-for="video in videos" :key="video.id">


Vue's reactivity

All explanation below applies to Vue2 only. Vue3 doesn't have any of these caveats.

If your state is not getting updated in the view, perhaps you are not exploring Vue at its best:

To have Vue automatically react to value changes, the objects must be initially declared in data. Or, if not, they must be added using Vue.set().

See the comments in the demo below. Or open the same demo in a JSFiddle here.

new Vue({
  el: '#app',
  data: {
    person: {
      name: 'Edson'
    }
  },
  methods: {
    changeName() {
      // because name is declared in data, whenever it
      // changes, Vue automatically updates
      this.person.name = 'Arantes';
    },
    changeNickname() {
      // because nickname is NOT declared in data, when it
      // changes, Vue will NOT automatically update
      this.person.nickname = 'Pele';
      // although if anything else updates, this change will be seen
    },
    changeNicknameProperly() {
      // when some property is NOT INITIALLY declared in data, the correct way
      // to add it is using Vue.set or this.$set
      Vue.set(this.person, 'address', '123th avenue.');
      
      // subsequent changes can be done directly now and it will auto update
      this.person.address = '345th avenue.';
    }
  }
})
/* CSS just for the demo, it is not necessary at all! */
span:nth-of-type(1),button:nth-of-type(1) { color: blue; }
span:nth-of-type(2),button:nth-of-type(2) { color: red; }
span:nth-of-type(3),button:nth-of-type(3) { color: green; }
span { font-family: monospace }
<script src="https://unpkg.com/vue@2"></script>

<div id="app">
  <span>person.name: {{ person.name }}</span><br>
  <span>person.nickname: {{ person.nickname }}</span><br>
  <span>person.address: {{ person.address }}</span><br>
  <br>
  <button @click="changeName">this.person.name = 'Arantes'; (will auto update because `name` was in `data`)</button><br>
  <button @click="changeNickname">this.person.nickname = 'Pele'; (will NOT auto update because `nickname` was not in `data`)</button><br>
  <button @click="changeNicknameProperly">Vue.set(this.person, 'address', '99th st.'); (WILL auto update even though `address` was not in `data`)</button>
  <br>
  <br>
  For more info, read the comments in the code. Or check the docs on <b>Reactivity</b> (link below).
</div>

To master this part of Vue, check the Official Docs on Reactivity - Change Detection Caveats. It is a must read!

acdcjunior
  • 132,397
  • 37
  • 331
  • 304
  • Sorry, I had updated the above to just use the store state instead and forgot to add the: `this.videos = this.getFreeVideo` I will update above. – Jordan Mar 19 '18 at 03:24
  • Oh man, I did not think to just use the getter directly like that in the v-for. Now will I need the `watch` for that as well to update/rerender? – Jordan Mar 19 '18 at 03:35
  • No, no, with the watcher you can use `videos` in the `v-for`. – acdcjunior Mar 19 '18 at 03:44
  • Hmm, using getFreeVideo directly now, but can't get the update even though I can see the new data in getFreeVideo in Vue dev tools. Not sure what is going on. – Jordan Mar 19 '18 at 03:54
  • Actually if I use this `
    ` and don't reference anything in data(), do I lose reactivity?
    – Jordan Mar 19 '18 at 04:10
  • No, you don't lose reactivity. The `getFreeVideo`computed only depends on the state of the store. Look, here's a fiddle I used: https://jsfiddle.net/acdcjunior/gumuxs3z/120/ as you can see, both work and get updated. Can you try to edit it and make this fiddle as close as possible to your case? – acdcjunior Mar 19 '18 at 04:12
  • Alright, I got it working. The video.js does not get instantiated the way I need when I go direct, but it updates. When I use the videos way it gets stuck, but this seems to be some sort of other issue at this point. I'll mark this as the solution, and then post more info in my own answer if I get it all fully resolved. – Jordan Mar 19 '18 at 04:49
  • This solution clued me to the fact I had a local property of the same as the vuex prop name defined in my component. Sometimes you can't see the forest for trees. – varontron Oct 03 '19 at 23:55
  • though in my case there was single video url and I had added key in the loop that was still not updating UI, but adding key to video tag updated that, meanwhile the solution was working fine on text fields – Bipin Chandra Tripathi Nov 26 '19 at 06:09
  • I believe the most important point from this answer is: use id of object in the v-for loop whenever possible and only use index as a last resort – Lars Ladegaard Dec 22 '20 at 08:15
  • *"To master this part of Vue, check the Official Docs on Reactivity - Change Detection Caveats. It is a must read!"*. Important to note none of the caveats are present in Vue3! – spinkus Nov 10 '22 at 02:21
5

So turns out the whole issue was with video.js. I'm half tempted to delete this question, but I would not want anyone who helped to lose any points.

The solution and input here did help to rethink my use of watchers or how I was attempting this. I ended up just using the central store for now since it works fine, but this will need to be refactored later.

I had to just forego using video.js as a player for now, and a regular html5 video player works without issues.

Jordan
  • 1,650
  • 2
  • 18
  • 17
2

If the computed property isn't referenced (e.g. "used") somewhere in your template code vue will skip reactivity for it.

First it's a bit confusing the way you're structuring the store and the state properties.

I would:

1) Have a "videos" property in the store state

2) Initialise it as an empty array

3) On application start populate it correctly with the "load" defaults, with a mutation that pushes the "default" video to it

4) Have the components mapGetter to it under the name of videos

5) Whenever you load a component that "updates" the possible videos, then dispatch the action and call the appropriate mutation to substitute the store "videos" property

Note: If components can have different "default" videos, then probably you'll want to have a videos property in the store that is initialised as false. This then allows you to have a computed property that uses the getter for the videos property in the store and in case it is false

What I mean is, for the first case

// store
state: {
  videos: []
}

getters: {
  videos(state) { return state.videos } 
}

//components

...
computed: {
  videos() {
    this.$store.getters.videos
  }
}

For the second case


// store
state: {
  videos: false
}

getters: { personal_videos(state) { return state.videos } }

//components
data() { return { default: default_videos } },
computed: {
  ...mapGetters([ 'personal_videos' ]),
  videos() {
    if (this.personal_videos) {
      return this.personal_videos
    } else {
      return this.default
    } 
  }

}

Personal: give them better names -_- and the first option is the clearest

ElectRocnic
  • 1,275
  • 1
  • 14
  • 25
m3characters
  • 2,240
  • 2
  • 14
  • 18
  • Ok, maybe I misunderstood how mapState could be used then from an example I had found here. – Jordan Mar 19 '18 at 02:17
  • If I'm not mistaken, because some parts of the code are missing, your `videos` data isn't being sourced directly from the getter you have in the store, instead you have a computed property that returns an assignment (?) to it. If this computed property isn't referenced anywhere, then the assignment won't happen. The easiest solution is to make your data.videos property map directly to a getter that returns whatever videos you have in stored in "store". This would be set in the store at init and updated when you call the api, then the mapGetter would automatically be reactive and always updated – m3characters Mar 19 '18 at 02:23
  • Ok, that makes sense. I was hoping to not do that since I am using this video player in a number of different places and the default initial video would not be the same in all of those, but what you are saying is making sense. I just need to make sure that the data for that state is available at the right time when everything loads or I end up with a player that has the content but does not have an instantiated video.js player. Thanks – Jordan Mar 19 '18 at 02:27
  • Perhaps you should change the way you're setting the initial videos? For instance calling an action on load that sets them initially on the store... And then using the getters as intended, because the way you're using it is the opposite of the purpose of having a "central" state store... – m3characters Mar 19 '18 at 02:32
  • I see your point from the code you are seeing, but eventually I'll be returning other videos and then checking for the ones I need. I may need to rethink the pattern at this point. I did have this setup only locally at first. I just thought I'd try it this way to see if it made it easier to work with. At this point a central store is not really needed for this component, but even when I had the data locally I could not get it to re-render. Like I stated, I was not doing this in via the computed property. I had actually tried to just update using async await, but never got it working. – Jordan Mar 19 '18 at 02:42
  • Alright, moved everything to the store instead like you suggested. That works fine for the initial load. I'm still not seeing the change needed though, even though Vue dev tools shows the correct updated data in getFreeVideo Vuex binding. So at this point, it seems like I need to debug why the state change does not trigger a re-render. – Jordan Mar 19 '18 at 02:52
  • I'm going to change the answer because it's easier than writing in the comments – m3characters Mar 19 '18 at 03:04
  • "If the computed property isn't referenced (e.g. "used") somewhere in your template code vue will skip reactivity for it." I've been stuck at reactivity for a day already, copied a lot of code from other .vue files. Idk if it was supposed to work before, but the above tip helped me... Ty! – Craws Jun 26 '21 at 01:44