Search code examples
typescriptvue.jsreactive-programming

Vue class objects with internal ref breaks with reactive


First of all, I love Vue 3. I really enjoy it over many other frameworks. But I think I have found a limitation. I am trying to wrap some of my very complicated logic in class where internally each instance does the dirty work. This feels more natural for me. But when my instances are wrapped with reactive(), everything seems to break.

Here is an example:

export class Container {
  public stat: Ref<Stat> = ref({ cpu: 0 });
  private _throttledStatHistory: UseThrottledRefHistoryReturn<Stat, Stat>;

  constructor(
    public readonly id: string,
    // more fields not shown...
  ) {
    this._throttledStatHistory = useThrottledRefHistory(this._stat, { capacity: 300, deep: true, throttle: 1000 });
  }

  public statHistory: ComputedRef<UseRefHistoryRecord<Stat>[]> = computed(
    () => this._throttledStatHistory.history.value
  );
}

I can use this object with something like

const container = new Container("123")
container.stat.value = { cpu: 1}
container.stat.value = { cpu: 2}

However, when using reactive like so:

const myRef = reactive(new Container("123"))

Everything seems to break:

myRef.value.stat.value // stat is no longer a ref. It is now a proxy
myRef.value.stat.value = {cpu: 3} // also breaks as statHistory is not updated at all 

My assumption is that everything being wrapped in reactive breaks.

Somethings can be fixed with toRaw() like toRaw(myRef.value).stat.value = ... but that feels unnatural.

Note if I make stat private with private stat: Ref<Stat> = ref({ cpu: 0 }); then the problem still persists. I expected private members to not be affected by reactive.

I am going crazy debugging this. What is the proper way to do class with internal reactivity?

Thanks!


Solution

  • Thanks Estus for suggestions.

    Looks like there are two options that worked for me:

    1. Use shallow reactive like
    const myRef = shallowReactive(new Container("123"))
    

    This works, but the con is that all my other code needs to be aware of using shallow.

    1. Use markRaw
    export class Container {
      public stat: Ref<Stat>;
      private throttledStatHistory: UseThrottledRefHistoryReturn<Stat, Stat>;
    
      constructor(
        public readonly id: string,
        // skipped
      ) {
        this.stat = markRaw(ref({ cpu: 0, memory: 0, memoryUsage: 0 }));
        this.throttledStatHistory = markRaw(
          useThrottledRefHistory(this.stat, { capacity: 300, deep: true, throttle: 1000 })
        );
      }
    
      public getStatHistory() {
        return this.throttledStatHistory.history.value;
      }
    }
    

    I liked the second option because it doesn't depend on shallow being used properly.