VueJS 3 - make reactive aware of new object properties - vue.js

There's a similar question here, but for Vue2. The solution, use Vue.set, is not valid in Vue3. I'm getting a lot of results about Vue2, but nothing about Vue3 for this yet.
There's a lot of fanciness going on in the code, but it's really quite moot. My object lives in a state in the vuex store. It has a non-enumerable property that is a function that adds records to itself like so:
class myObject extends Object {
constructor(){
this.add = function(id){
isReactive(this) //=== true, because vuex
this[id] = new myOtherObject(data);
}
}
}
Question:
Is there a way to force the reactive wrapping this object to know it's had these properties added?
There are other work arounds to my problem, one of which I've implemented, and is basically to include some other thing in the computed so it knows it's time to update itself. Luckily, I have a non-enumerable length property on my object that is incremented when a property is added that works perfectly:
const record = computed(() => {
store.state.myData.myObject.length; //<- literally just an incremented ref
return store.state.myData.myObject; //<- the problem child
});
But this is weird and hacky, and feels very anti-pattern.
Example code:
class myOtherObject extends Object {};
class myObject extends Object {
constructor(){
super();
this.add = function(id){
console.log(`Added: ${id}`);
this[id] = new myOtherObject();
}
}
}
const store = Vuex.createStore({
state: function(){
return {
myData: {
test1: new myObject()
}
}
}
});
const record = Vue.computed(() => {
console.log("This isn't happening when new props are added");
return store.state.myData.test1; //<- the problem child
});
for (var i = 0; i < 100; ++i){
store.state.myData.test1.add(i);
}
console.log(store.state.myData.test1);
<script src="https://unpkg.com/vue#3.2.30/dist/vue.global.js"></script>
<script src="https://unpkg.com/vuex#4.0.2/dist/vuex.global.js"></script>

Related

vue3 resuable computed properties

I tried creating Singleton service files with vue3 to have reusable computed properties (define once , then just reuse on every other call)
Normally with composition api pattern each time you do useX() every computed property inside is is redefined, this is ok for small project with little amount of computed props but for my scenario many computed props are reused on multiple components
so here what I did :
function factory(){
const x = computed(()=> store.getters['x']);
function updateX(){
store.commit('setX',newXValue);
}
return { x };
}
export default function useY() {
return SingletonServiceProvider.Create<ReturnType<typeof factory>>(factory);
}
export default class SingletonServiceProvider {
static Create<T>(factory: (...args: unknown[]) => T): T {
// TODO define proper type
// #ts-ignore
if (!factory.Instance) {
// #ts-ignore
factory.Instance = factory();
}
// #ts-ignore
return factory.Instance;
}
}
Now the problem I am facing: the x prop is properly filled the first time. But If I change the store value the x computed property is not updated!!!
I even added a store.watch and even this is nor triggered too!!
store.watch(
(state, getters) => getters['x'],
(value, oldValue) => {
console.log('...................xxxxx', { ...value }, { ...oldValue });
},
);
What am I doing wrong? Is this a vue/vuex limit ?

extending of observable subclassing: [MobX] Options can't be provided for already observable objects

When I create a deep structure with few extends I got this kind of error:
Uncaught Error: [MobX] Options can't be provided for already observable objects.
"mobx": "^6.4.2",
"mobx-react-lite": "^3.3.0",
All of code is just dirty example. real structure more complex.
Code example:




import { makeObservable, observable, action } from 'mobx';
class DateMobx {
date ;
constructor(data) {
this.date = new Date(data.date);
makeObservable(this, { date: observable }, { autoBind: true });
}
}
class Todo extends DateMobx {
id = 0;
title = 'title';
text = 'text';
constructor(todo) {
super(todo);
this.id = todo.id;
this.title = todo.title;
makeObservable(this, { id: observable, title: observable, changeTitle: action }, { autoBind: true });
}
changeTitle= (e) => {
const { value } = e.target;
this.title = value;
}
}
class TodoList {
todo = [];
constructor() {
const todoData = [{ id: 1, title: 'title', date: '123' }];
this.todo = todoData.map(item => new Todo(item)); // ERROR happen here
makeObservable(this, { todo: observable }, { autoBind: true });
}
}


Error happen in constructor of TodoList class.

if remove makeObservable from Todo class, error is not reproduced but I need reactivity in that class.

If remove extends DateMobx from Todo class error also is not reproduces (but I have a lot of general classes with basic logic where I need reactivity too).



Why its happen and what should I do if I really need such kind of deep structure ?
Guys on github bring me the right solution
Don't pass the third param ({ autoBind: true }) to makeObservable in subclasses. All options are "inherited" and can't be changed by subsequent makeObservable calls on the same object (this).
options argument can be provided only once. Passed options are
"sticky" and can NOT be changed later (eg. in subclass).
https://mobx.js.org/observable-state.html#limitations

Vuex-module-decorator, modifying state inside an action

Using the vuex-module-decorator I have a authenticate action that should mutate the state.
#Action
public authenticate(email: string, password: string): Promise<Principal> {
this.principal = null;
return authenticator
.authenticate(email, password)
.then(auth => {
const principal = new Principal(auth.username);
this.context.commit('setPrincipal', principal);
return principal;
})
.catch(error => {
this.context.commit('setError', error);
return error;
});
}
// mutations for error and principal
But this fail with the following message:
Unhandled promise rejection Error: "ERR_ACTION_ACCESS_UNDEFINED: Are you trying to access this.someMutation() or this.someGetter inside an #Action?
That works only in dynamic modules.
If not dynamic use this.context.commit("mutationName", payload) and this.context.getters["getterName"]
What I don't understand is that it works well with #MutationAction and async. However I miss the return type Promise<Principal>.
#MutationAction
public async authenticate(email: string, password: string) {
this.principal = null;
try {
const auth = await authenticator.authenticate(email, password);
return { principal: new Principal(auth.username), error: null };
} catch (ex) {
const error = ex as Error;
return { principal: null, error };
}
}
--
At this time I feel blocked and would like to have some help to implement an #Action that can mutate the state and return a specific type in a Promise.
Just add rawError option to the annotation so it becomes
#Action({rawError: true})
And it display error normally. this is because the the library "vuex-module-decorators" wrap error so by doing this you will able to get a RawError that you can work with
You can vote down this answer if you would like because it isn't answering the specific question being posed. Instead, I am going to suggest that if you are using typescript, then don't use vuex. I have spent the past month trying to learn vue /vuex and typescript. The one thing I am committed to is using typescript because I am a firm believer in the benefits of using typescript. I will never use raw javascript again.
If somebody would have told me to not use vuex from the beginning, I would have saved myself 3 of the past 4 weeks. So I am here to try and share that insight with others.
The key is Vue 3's new ref implementation. It is what really changes the game for vuex and typescript. It allows us to not have to rely on vuex to automatically wrap state in a reactive. Instead, we can do that ourselves with the ref construct in vue 3. Here is a small example from my app that uses ref and a typescript class where I was expecting to use vuex in the past.
NOTE1: the one thing you lose when using this approach is vuex dev tools.
NOTE2: I might be biased as I am ported 25,000 lines of typescript (with 7000 unit tests) from Knockout.js to Vue. Knockout.js was all about providing Observables (Vue's ref) and binding. Looking back, it was kind of ahead of its time, but it didn't get the following and support.
Ok, lets create a vuex module class that doesn't use vuex. Put this in appStore.ts. To simplify it will just include the user info and the id of the club the user is logged into. A user can switch clubs so there is an action to do that.
export class AppClass {
public loaded: Ref<boolean>;
public userId: Ref<number>;
public userFirstName: Ref<string>;
public userLastName: Ref<string>;
// Getters are computed if you want to use them in components
public userName: Ref<string>;
constructor() {
this.loaded = ref(false);
initializeFromServer()
.then(info: SomeTypeWithSettingsFromServer) => {
this.userId = ref(info.userId);
this.userFirstName = ref(info.userFirstName);
this.userLastName = ref(info.userLastName);
this.userName = computed<string>(() =>
return this.userFirstName.value + ' ' + this.userLastName.value;
}
}
.catch(/* do some error handling here */);
}
private initializeFromServer(): Promise<SomeTypeWithSettingsFromServer> {
return axios.get('url').then((response) => response.data);
}
// This is a getter that you don't need to be reactive
public fullName(): string {
return this.userFirstName.value + ' ' + this.userLastName.value;
}
public switchToClub(clubId: number): Promise<any> {
return axios.post('switch url')
.then((data: clubInfo) => {
// do some processing here
}
.catch(// do some error handling here);
}
}
export appModule = new AppClass();
Then when you want to access appModule anywhere, you end up doing this:
import { appModule } from 'AppStore';
...
if (appModule.loaded.value) {
const userName = appModule.fullName();
}
or in a compositionApi based component. This is what would replace mapActions etc.
<script lang="ts">
import { defineComponent } from '#vue/composition-api';
import { appModule } from '#/store/appStore';
import footer from './footer/footer.vue';
export default defineComponent({
name: 'App',
components: { sfooter: footer },
props: {},
setup() {
return { ...appModule }
}
});
</script>
and now you can use userId, userFirstName, userName etc in your template.
Hope that helps.
I just added the computed getter. I need to test if that is really needed. It might not be needed because you might be able to just reference fullName() in your template and since fullName() references the .value variables of the other refs, fullName might become a reference itself. But I have to check that out first.
I sugest this simple solution, work fine for me 👌:
// In SomeClassComponent.vue
import { getModule } from "vuex-module-decorators";
import YourModule from "#/store/YourModule";
someMethod() {
const moduleStore = getModule(YourModule, this.$store);
moduleStore.someAction();
}
If the action has parameters, put them.
Taken from: https://github.com/championswimmer/vuex-module-decorators/issues/86#issuecomment-464027359

How to observe/subscribe to changes in the state in Aurelia Store?

I have a property selectedOption on the state of my Aurelia Store, which can be changed via actions. I want to observe/subscribe to any changes to this property on the state. My problem is the subscription within the BindingEngine doesn't work because every time you change the state, you create a new copy of the state, therefore the subscription no longer works.
Here is my example:
import { Disposable, BindingEngine, autoinject } from "aurelia-framework";
import { connectTo, dispatchify } from "aurelia-store";
#autoinject()
#connectTo()
export class Holiday {
subscription: Disposable;
state: any;
constructor(private bindingEngine: BindingEngine) {
}
async updateState()
{
await dispatchify(changeSelectedOption)();
}
attached() {
this.subscription = this.bindingEngine
.propertyObserver(this.state, 'selectedOption')
.subscribe((newValue, oldValue) => {
console.log("something has changed!")
});
}
}
export class State {
selectedOption: number = 0;
}
export const changeSelectedOption = (state: State) => {
let updatedState = { ...state };
updatedState.selectedOption++;
return updatedState;
}
store.registerAction("changeSelectedOption", changeSelectedOption);
The first time, my subscription will work and the console will log "something has changed!" as the state is the same object, but it won't work after.
Another solution I could use would be to have a computed property like so:
#computedFrom("state.selectedOption")
get selectedOptionChanged()
{
return console.log("something has changed!");
}
This is a hack, and this computed won't ever be triggered as it is not bound to anything in the HTML.
For context, I want to trigger a server call every time the selectedOption property changes.
What can I do to receive all updates from the property on the state?
The thing here is that the state observable exposed by the Store is a RxJS stream. So with the advent of the new "multi-selector" feature for connectTo you could create two bindings. By implementing a hook called selectorKey Changed, in your sample selectedOptionChanged it would get called on every change of said property.
#connectTo({
selector: {
state: (store) => store.state, // the complete state if you need
selectedOption: (store) => store.state.pluck("selectedOption")
}
})
class MyVM {
...
selectedOptionChanged(newState, oldState) {
// notification about new state
}
}
Instead of store.state.pluck("selectedOption") you can also experiment with additional conditions when to notify about changes like adding distinctUntilChanged etc.
Read more about multi-selectors in the updated docs.
Alternatively if you don't want to use the connectTo decorator, simply use the state property and create another subscription

Vue make ES6 class object reactive

I have a ES6 class that I use to hold and manage datas:
import DataManager from "./components/data-manager.js";
export default {
...
data() {
return {
dataModel: null
}
},
beforeMount() {
this.dataModel = new DataManager();
},
computed: {
datas() {
return this.dataModel.myProperty
}
},
...
}
But it appears that there is a reactivity issue and any changes made into that dataModel does not trigger the re-rendering of the view.
How can I make the properties of my class reactive ?
Thanks,
Edit: #acdcjunior The Class looks like this, but I realize that some mutation occurs... would that be the issue ? Anyway if thats not good practice to make a whole ES6+ class reactive, I'll get ride of that and go for a plain Object, or even Vuex store. Any simple suggestion ?
class DataManager {
constructor() {
this.professionalsList = [];
this.eventsList = [];
this.attendeesList = [];
}
// import datas
importDatas({
professionalsList,
eventsList,
attendeesList
}) {
this.professionalsList = this.parsePros(professionalsList)
...
}
parsePros(list) {
return list.map( item => { ... })
}
...
}
Edit #2: So the issue was indeed some mutations that occures in one of the objects I was trying to bind.
You need to store the same data structure in data().dataModel as returned from DataManager() and change every property in methods. When you setting new dynamic property in data object they can't be reactive. You can try Vue.set(), but it's a bad practice.