5

I was witnessing some odd behaviour while building my app where a part of the dom wasn't reacting properly to input. The mutations were being registered, the state was changing, but the prop in the DOM wasn't. I noticed that when I went back, edited one new blank line in the html, came back and it was now displaying the new props. But I would have to edit, save, the document then return to also see any new changes to the state.

So the state was being updated, but Vue wasn't reacting to the change. Here's why I think why: https://vuejs.org/v2/guide/reactivity.html#For-Objects

Vue cannot detect property addition or deletion. Since Vue performs the getter/setter conversion process during instance initialization, a property must be present in the data object in order for Vue to convert it and make it reactive

Sometimes you may want to assign a number of properties to an existing object, for example using Object.assign() or _.extend(). However, new properties added to the object will not trigger changes. In such cases, create a fresh object with properties from both the original object and the mixin object

The Object in my state is an instance of js-libp2p. Periodically whenever the libp2p instance does something I need to update the object in my state. I was doing this by executing a mutation

syncNode(state, libp2p) {
    state.p2pNode = libp2p
}

Where libp2p is the current instance of the object I'm trying to get the DOM to react to by changing state.p2pNode. I can't use $set, that is for single value edits, and I think .assign or .extend will not work either as I am trying to replace the entire object tree.

Why is there this limitation and is there a solution for this particular problem?

Community
  • 1
  • 1
AustinFoss
  • 305
  • 3
  • 12
  • Here is the repo for my project so that you can see the behaviour in action: https://github.com/EruGuru/p2pdnd.git – AustinFoss Apr 02 '20 at 04:24
  • Once both terminals are running open two tabs of the same instance, this will create two nodes that will connect to each other using the relay server – AustinFoss Apr 02 '20 at 04:42

3 Answers3

2

I believe your issue is more complex than the basic rules about assignment of new properties. But the first half of this answer addresses the basics rules.

And to answer why Vue has some restrictions about how to correctly assign new properties to a reactive object, it likely has to do with performance and limitations of the language. Theoretically, Vue could constantly traverse its reactive objects searching for new properties, but performance would be probably be terrible.

For what it's worth, Vue 3's new compiler will supposedly able to handle this more easily. Until then, the docs you linked to supply the correct solution (see example below) for most cases.

var app = new Vue({
  el: "#app",
  data() {
    return {
      foo: {
        person: {
          firstName: "Evan"
        }
      }
    };
  },
  methods: {
    syncData() {
      // Does not work
      // this.foo.occupation = 'coder';

      // Does work (foo is already reactive)
      this.foo = {
        person: {
          firstName: "Evan"
        },
        occupation: 'Coder'
      };

      // Also works (better when you need to supply a 
      // bunch of new props but keep the old props too)
      // this.foo = Object.assign({}, this.foo, {
      //  occupation: 'Coder',
      // });      
    }
  }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  Hello {{foo.person.firstName}} {{foo.occupation}}!
  <button @click="syncData">Load new data</button>
</div>

Update: Dan's answer was good - probably better than mine for most cases, since it accounts for Vuex. Given that your code is still not working when you use his solution, I suspect that p2pNode is sometimes mutating itself (Vuex expects all mutations in that object to go through an official commit). Given that it appears to have lifecycle hooks (e.g. libp2p.on('peer:connect'), I would not be surprised if this was the case. You may end up tearing your hair out trying to get perfect reactivity on a node that's quietly mutating itself in the background.

If this is the case, and libp2p provides no libp2p.on('update') hook through which you could inform Vuex of changes, then you might want to implement a sort of basic game state loop and simply tell Vue to recalculate everything every so often after a brief sleep. See https://stackoverflow.com/a/40586872/752916 and https://stackoverflow.com/a/39914235/752916. This is a bit of hack (an informed one, at least), but it might make your life a lot easier in the short run until you sort out this thorny bug, and there should be no flicker.

AlexMA
  • 8,952
  • 5
  • 39
  • 55
  • I just added a reproducible example as my github link in the comment under my OP. That's good news to hear about the compiler. – AustinFoss Apr 02 '20 at 04:36
  • You are correct. After some more experimentation just before bed I fixed it with `Vue.set(state, 'p2pNode', null);` before setting it to libp2p. I really don't like this solution because it creates a weird sort of flicker but I guess it works! – AustinFoss Apr 02 '20 at 04:45
  • If you have the time Alex, could you shed some light on why Dan's, from one of the other answers, example works to replace an object of multiple branches but my github does not? – AustinFoss Apr 02 '20 at 04:51
  • @Eru I updated the question. Very cool app btw, thanks for sharing the source code. – AlexMA Apr 02 '20 at 13:28
  • Thanks Alex! I really don't think it's that cool yet. All I did was tweak an existing example from the js-libp2p repo and then strap it to vuex skeleton. Connections are made. Now to start sending data between tabs. I only just got the disconnect to register properly. The repo as is actually displaying a list of the peers it's found to date, not the actual connected peers. – AustinFoss Apr 03 '20 at 17:26
2

The only thing needed to reassign a Vuex state item that way is to have declared it beforehand.

It's irrelevant whether that item is an object or any other variable type, even if overwriting the entire value. This is not the same as the reactivity caveat situations where set is required because Vue can't detect an object property mutation, despite the fact that state is an object. This is unnecessary:

Vue.set(state, 'p2pNode', libp2p);

There must be some other problem if there is a component correctly using p2pNode that is not reacting to the reassignment. Confirm that you declared/initialized it in Vuex initial state:

state: {
  p2pNode: null  // or whatever initialization value makes the most sense
}

Here is a demo for proof. It's likely that the problem is that you haven't used the Vuex value in some reactive way.

Dan
  • 45,062
  • 13
  • 59
  • 80
  • Can confirm the both with an empty object, `{}`, or `null` this did not not work. I think it is because there is no root element of the object in your example. From the vue docs I link in my OP they state that vue cannot react to a change of an object with a root element. If you have an example working that reacts to an object tree with only a few branches changed that's what I'm looking to do. My github is now linked in a comment attached to my OP. – AustinFoss Apr 02 '20 at 04:28
  • The root element in both of our examples is `state` itself. You stated that `p2pNode`, a property of state, was not reactive when reassigned. My demo uses the scenario you set up and shows that it is. What root element does the scenario have that I didn't use? – Dan Apr 02 '20 at 04:39
  • Please look at my github and it will make more sense. P2pNode is the root element of the object some of which branches i'm trying to update by just replacing the whole object at once. It's the complexity of this object tree that makes it so that Vue cann't react. Alex's comment explains why that is. – AustinFoss Apr 02 '20 at 04:40
  • Ok, please check my updated demo and explain if I'm missing the point you're trying to make. – Dan Apr 02 '20 at 04:46
  • I combined your line of code with alex's suggestion to create a new one everytime. I had to set it to null everytime, not just at the base state, prior to updating the new libp2p instance. Now it works. – AustinFoss Apr 02 '20 at 04:47
  • I just checked you demo and I'm not sure why that is then. If @AlexMA from the first answer might be able to explain what the difference is between that and my github. – AustinFoss Apr 02 '20 at 04:50
  • His answer has everything to do with my question. It was a combination of both of your answers that ultimately worked. – AustinFoss Apr 02 '20 at 04:53
  • @Dan I agree your answer is better than my initial one for most cases with Vuex. Upvoted. – AlexMA Apr 02 '20 at 13:29
  • @AlexMA Hi, thank you, it's ok. FYI what I showed here is still the correct explanation in this thread, even without Vuex. Vue reassignment is *inherently reactive* and doesn't suffer caveats, which are only for changing **properties**. OP's guess about why his initial code didn't work was wrong and got you on the wrong track. [Even your example will show this](https://jsfiddle.net/sh0ber/zbtnphwL/) – Dan Apr 02 '20 at 13:50
  • @Dan you're completely correct. I rewrote my answer. I got confused reading Vue's reactivity in depth section last night. – AlexMA Apr 02 '20 at 14:18
  • @Dan I would not be offended if he accepted your answer, but I do think my suggestion about his particular app's libp2p peculiarities might be correct for his specific bug. – AlexMA Apr 02 '20 at 14:29
  • @AlexMA It's ok, thanks again for your vote & response – Dan Apr 02 '20 at 14:42
  • If you go to the repo right now and run the program following the read me, open two tabs, select player character, both tabs will update with the other peer ID. But if you go to /src/store/index.js and comment out line 34 where I "flicker" in a brief "null" value for p2pNode before using your set solution. But it doesn't work on it's own. Alex's suggestion to "to implement a sort of basic game state loop and simply tell Vue to recalculate everything every so often" for every libp2p.on event that get's it working. I'll even give your post the solution as well because it was a team effort. – AustinFoss Apr 03 '20 at 17:14
0

Just a thought, I don't know anything about libp2p but have you try to declare your variable in the data options that change on the update:

data: {
    updated: ''
  }

and then assigning it a value :

syncNode(state, libp2p) {
    this.updated = state
    state.p2pNode = libp2p
}
jcoleau
  • 757
  • 4
  • 16
  • I think you would have to rearrange the order of that for the logic to make sense. As `libp2p` is the first thing to see changes to the instance the order to pass those changes along would have to be libp2p > state > data.updated. But unfortunately I did try that already without success. Rather tired now, and may give this method a second shot tomorrow. If you'd like to see what I'm actually looking at my github is now linked in the comment under my OP. – AustinFoss Apr 02 '20 at 04:34