2

I'm trying to make a class instance property reactive to display the error message if authentication fails.

userModel.ts

export class User {
  private error: string;

  set errorMessage(errorMessage: string) {
    this.error = errorMessage;
  }
  get errorMessage() {
    return this.error
  }

// other stuff...
}

LoginView.vue

import { User } from "./models/userModel";
import { ref } from 'vue';

const user = new User();
const errorMessage = ref(user.errorMessage); // <--- This is not working.
const errorMessage = ref(user.error); // <--- Neither this even if user.error property is public.

No warnings or exceptions, reactive value just remains empty. What I am missing?

  • You should probably avoid setting only some properties of an object to be reactive and instead have the whole object be reactive. const user = reactive(new User); but if you insist on making this work, I figure the error must be elsewhere, since I reacreated your example and it works just fine. – Dr4jw3r Jul 29 '22 at 08:04

2 Answers2

7

Here, you are creating new reactive variables, with the initial value being the one in your class. Your class and your ref are unrelated.

If you want your class to have reactive properties, you have to instanciate them this way:

export class User {
  private error: Ref<string> = ref('');

  public errorMessage: WritableComputedRef<string> = computed({
    get() {
      return this.error.value
    },
    set(errorMessage: string) {
      this.error.value = errorMessage;
    },
  })
}

Side note: I'd recommend to use the "composables" approach instead of using classes, because class instances are not compatible with SSR.

export function useUser () {
  const user: User = reactive({})
  // other stuff...

  const error: Ref<string> = ref('');

  const errorMessage: WritableComputedRef<string> = computed({
    get() {
      return this.error.value
    },
    set(errorMessage: string) {
      this.error.value = errorMessage;
    },
  })

  return { user, errorMessage }
}
cwillinx
  • 537
  • 2
  • 13
Kapcash
  • 6,377
  • 2
  • 18
  • 40
  • Thank you for the answer, Kapcash. Have you defined your own 'Reactive' type here? I can't find it in the framework's API. – Egor Konovalov Jul 29 '22 at 10:46
  • 1
    Sorry, I didn't remember the correct type. I updated the code with the correct types. `reactive` returns a `UnwrapNestedRefs` which is just a fancy type to say it's a `User` without any `Ref` inside. – Kapcash Jul 29 '22 at 12:46
  • 2
    Notice that the use of composition API inside a class is a workaround that may cause more problems than it solves. This not just about reactive types. If `User` instance becomes reactive, e.g as a property of reactive object like a store, , `user` ref property is unwrapped. There will be `this.error`, not `this.error.value`, and errorMessage implementation becomes invalid – Estus Flask Jul 31 '22 at 07:37
  • The computed way of using class property really helped. I have been trying all kind of tricks to have internal ref but nothing seemed to work. – Amir Raminfar Oct 03 '22 at 16:19
  • I needed a `WritableComputedRef` instead of a `ComputedRef` – cwillinx Jul 25 '23 at 10:28
4

Accessing a primitive property by value like ref(user.error) can't be reactive, no matter it's a getter or not, in this case it doesn't differ from ref(undefined).

For own enumerable property it could be:

let user = reactive(new User());
let { error } = toRefs(user);

It won't handle property accessors, they need to be explicitly accessed with computed property:

let errorMessage = computed({
  get: () => user.errorMessage,
  set: value => { user.errorMessage = value }
});

Whether it's a getter or regular property doesn't affect how a computed works because an accessor is defined on class prototype and accesses reactive this.error (this can be a problem when non-reactive this is bound in a constructor, like is shown here and here).

Estus Flask
  • 206,104
  • 70
  • 425
  • 565