javascriptvue.jsvue-class-components

Vue data not available until JSON.stringify() is called


I'm not sure how to tackle this issue because there's quite a bit into it, and the behavior is one I've never seen before from JavaScript or from Vue.js Of course, I will try to keep the code minimal to the most critical and pieces

I'm using vue-class-component(6.3.2), so my Vue(2.5.17) components look like classes :) This particular component looks like so:

import GameInterface from '@/GameInterface';

class GameComponent extends Vue {
  public gameInterface = GameInterface();
  public mounted() {
    this.gameInterface.launch();
  }
}

GameInterface return an object with a launch method and other game variables.

In the game interface file to method looks something like this:

const GameInterface = function () {
  const obj = {
    gameState: {
      players: {},
    },
    gameInitialized: false,
    launch() => {
      game = createMyGame(obj); // set gameInitialized to true
    },
  };
  return obj;
}
export default GameInterface;

Great, it works, the object is passed onto my Phaser game :) and it is also returned by the method, meaning that Vue can now use this object.

At some point I have a getter method in my Vue class that looks like so:

get currentPlayer() {
  if (!this.gameInterface.gameInitialized) return null;

  if (!this.gameInterface.gameState.players[this.user.id]) {
    return null;
  }
  return this.gameInterface.gameState.players[this.user.id];
}

And sure enough, null is returned even though the player and id is clearly there. When I console.log this.user.id I get 4, and gameInterface.gameState.players returns an object with getters for players like so:

{
  4: { ... },
  5: { ... },
}

Alright, so it does not return the player even though the object and key are being passed correctly...

But I found an extremely strange way to "FIX" this issue: By adding JSON.parse(JSON.stringify(gameState)) like so

get currentPlayer() {
  // ...
  if (!this.gameInterface.gameState.players[this.user.id]) {
    // add this line
    JSON.stringify(this.gameInterface.gameState);
    return null;
  }
  return this.gameInterface.gameState.players[this.user.id];
}

It successfully returns the current player for us... Strange no?

My guess is that when we do this, we "bump" the object, Vue notices some change because of this and updates the object correctly. Does anyone know what I'm missing here?


Solution

  • After working on the problem with a friend, I found the underlying issue being a JavaScript-specific one involving Vue's reactive nature.

    https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
    In this section of the documentation, a caveat of Vue's change detection is discussed:

    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.

    When, in my game run-time, I set players like so:

    gameObj.gameState.players[user.id] = {...playerData}
    

    I am adding a new property that Vue has not converted on initialization, and Vue does not detect this change. This is a simple concept I failed to take into account when developing my game run-time.

    In order to correctly set a new player, I've decided to use the spread operator to change the entirety of the players object, which Vue is reacting to, and in turn, Vue will detect my player being added like so:

    gameObj.gameState.players = {
      ...gameObj.gameState.players,
      [user.id]: {...playerData}
    }
    

    Vue also discusses another method called $set, which you can read on the same page.