Vue class objects with internal ref breaks with reactive
Asked Answered
S

1

1

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!

Show answered 3/10, 2022 at 18:9 Comment(6)
See #73163514Paramorphism
Also #69050912 . This idea is faulty by design. Vue reactivity wasn't designed to be used with classes, you won't benefit from using them instead of plain objects. ref(new Container("123")) - can be solved with shallowRef, but this is just a symptom of a bigger problemParamorphism
Thanks. I saw those already. The problem is a little different because I am seeing my refs being converted to regular proxies which is unexpected.Show
I think you're right though. Vue wasn't designed for this use case. Which is unfortunate. I may have to figure out a way to move all the reactivity of stat to outside.Show
Your problem can be seen here https://mcmap.net/q/1483649/-how-to-make-a-class-getter-reactive-in-vue-js-3-with-composition-api . It's expected and documented, vuejs.org/guide/essentials/… . You'll have less problems when rewriting Container to regular composable function. Nothing here really requires a class.Paramorphism
So I was already using that solution with computed. Still didn't work. But reading the doc I found that it is documented for reactive to unwrap all refs. That's good to see. Then I saw shallowRef as you suggested and markRaw. I am going to play with those.Show
S
1

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.

Show answered 3/10, 2022 at 21:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.