Changes made to an object created outside of Vue component are not detected by Vue 3

Viewed 800

I have a class Character : Character.ts

    /// This is called when server responds
    public setAttributeByType(type: StatsTypes, value: number): void {
        switch (type) {
            case StatsTypes.STRENGTH:
            case StatsTypes.DEXTERITY:
            case StatsTypes.VITALITY:
            case StatsTypes.INTELIGENCE:
                this.stats[type] = value;
                break;
            default: break;
        }
    }
    ....

The class instance is created outside of Vue component in my "networking code":

    public static onStartGame(data:any):void {
        Player.character = new Character(data.data);
        Game.displayGamePage(PagesIndex.GAME_PAGE);
        requestAnimationFrame(Game.loop);
    }

And used in main component: Game.vue

import { defineComponent } from 'vue'
import Player from '@/Player/player';
import SceneManager, { Scenes } from '@/Render/scene_manager';
import Scene from '@/Render/scene';
import MainScene from "@/Render/scenes/main";
import MapScene from "@/Render/scenes/map";
import Game from '@/game/game';

// Components
import VplayerBar from "@/vue/subs/playerBar.vue"
import Vcharacter from "@/vue/subs/character.vue"

export enum GamePages {
    MAIN_PAGE = 1,
    MAP_PAGE,
}

export default defineComponent({
    name: "game",
    components: {
        VplayerBar,
        Vcharacter,
    },
    data() {
        return {
            page: GamePages.MAIN_PAGE,
            scenes: Scenes,
            gamePages: GamePages,
            player: Player,
            character: Player.character, /* <------------ Reference to class */
            pages: {
                character: false,
            }
        }
    },
})

...which pass it down as a prop to character.vue component

export default defineComponent({
    name: "character",
    props: {
        character: {  // <---- This prop does not change
            type: Character,
            required: true
        },
        toggleCharacter: {
            type: Function, 
            required: true
        }
    },
    components: {
        VBackpack,
        VInventory
    },
    data() {
        return {
            StatsTypes,
        }
    },
    methods: {
        toglePage() {
            this.toggleCharacter()
        },
        getPortrait(isMobile:boolean = false) {
            return Character.getCharacterPortrait(this.character.faction, this.character.gender, isMobile);
        },
        addPoint(attribute:StatsTypes, value: number) {
            if (GKeyHandler.keys[Keys.SHIFT_LEFT])
                value = 10;

            CharacterHandler.addAttributePoint(Player.character, attribute, value);
            //this.character.stats[StatsTypes.STRENGTH] += 1; 
        }
    }
});

Problem is that whenever I change anything in character class instance outside the vue component (in my networking code) - for example character.setAttributeByType(attribute, value), Vue does not see the change. If I do this directly inside character.vue component, it works (see commented code in addPoint)

I tried to use Proxy & Watch and it did not help.

1 Answers

Your problem is an "identity" issue described here

Vue 3 is using ES6 proxies to make objects reactive. If you do const data = reactive(payload), the data is different object then payload (unlike in Vue 2 where the object was just modified with reactive setters/getters).

Same applies for Options API (you are using). If you do character: Player.character in data() the result is this.character (inside Vue component) is different object then Player.character. You can easily test it by doing console.log(this.character === Player.character) ...for example in mounted() - result will be false

As a result if you make any change using this.character (Vue reactive proxy), Vue will detect the change and rerender (and propagate the change to the original object) but if you change the original object Player.character the change is not detected by Vue...

Simple fix is to use Vue's Composition API which allows you to use Vue reactivity outside the Vue components.

import { reactive } from `vue`

Player.character = reactive(new Character(data.data));

Now when you use Player.character to initialize data() in Vue component, Vue sees it is a reactive proxy already and do not wrap it in proxy again

Related