RxJS Operators with Aurelia (v1) Store

I am at a loss here - no doubt due to my lack of depth of understanding of the store and RxJS. I have converted an existing part of my app using the store as a way to understand the benefits. My first lack of understanding was that, if my component subscribes to store state updates, the subscriptions get fired EVERY time the state changes even though I may only be interested in a single state property which may not have changed. I want it to behave more like an observable so that I react when changes I am interested in happen, but ignore it otherwise. Enter RxJS filtering with distinctUntilChanged and distinctUntilKeyChanged. Fantastic, but I simply cannot get it to work in a consistent way.

My State Definition

    export interface IAppState {
        profile: UserProfile;
        agreements: Array<Agreement>;
        newAgreement:Agreement;
        newAgreementState:NewAgreementState;
    }

NewAgreementState object property

export class NewAgreementState {
    public agreementId:string = "";
    public currentState:number = 0;
}

When I subscript to state changes in a component using the distinctUntilChanged filter I only see changes when I observe the primitive values. The subscription below shows the correct comparison between previous and new value.

     const sub3 = this.store.state.pipe(pluck("newAgreementState"),pluck("currentState"),distinctUntilChanged((x,y) => {
                this.logger.debug("aurelia store - compare primitive values: ", x, y);
                return x === y;
            }))
            .subscribe((s) => {
                this.logger.debug("aurelia store - primitive value changed: ", s);          
            });
        this.subscriptions.push(sub3);

If I create a second subscription and use distinctUntilKeyChanged I do not detect any changes. It sees previous and current values the same - both the updated state values

     const sub2 = this.store.state.pipe(pluck("newAgreementState"),distinctUntilKeyChanged("currentState",(x,y) => {
                this.logger.debug("aurelia store - compare key values: ", x, y);
                return x === y;
            }))
            .subscribe((s) => {
                this.logger.debug("aurelia store - key values changed: ", s.currentState);          
            });
        this.subscriptions.push(sub2);

Can anyone point me the the right direction??? Is there something I need to do to my State classes which will make it work>

Any help very much appreciated.

Jonathan

1 Like

Hey there,

can you show an Action you dispatched to modify newAgreementState? I suspect the issue in the way the next state is constructed and returned there.

On another note, pluck can navigate itself so no need for two plucks: pluck("newAgreementState", "currentState")

2 Likes

Hi Zewa666

Thanks so much. As soon as I read your comment I realised. In my actions, I am only doing a shallow copy of my state object using the spread operator. This will mean that any object properties do not get copied - merely referenced. This would explain why only primitive changes are reported.

Am I right?

Thank you so much for your input - and the tip about the pluck operator.

Many thanks,

Jonathan

1 Like

Nice one, exactly what I thought but didnt want to burry you with terms like shallow clones, mutated states etc. Great you figured it out and also by doing so you removed one more potential future issue in case you plan to introduce time-travelling.

1 Like

Hi Zewa666,

Thanks again!

I was so pleased to discover Immer in the process. However, I am still getting the behaviour which is very weird. I have not changed my State definition at all - so same as above.

Here is where I dispatch the action:

public goToStateId(id: number) {    
            if (this.canGoToStateId(id)){
                this.logger.debug("Navigating to step", id);
                this.store.dispatch(currentNewAgreementState, id);
            }
        }

Here is the new refactored action using Immer:

export const currentNewAgreementState = (state: IAppState, stateId:number) => {
    return produce(state, draftState => {
        const idx = draftState.newAgreementState.states.findIndex(s => s.id === stateId);
        if ( idx !== -1 ){
            draftState.newAgreementState.states.forEach(s => s.active = false);
            draftState.newAgreementState.currentState = stateId;
            draftState.newAgreementState.states[idx].active = true;
        }
    });
};

Do you have any ideas?

Many thanks

1 Like

I remember there was an issue with using Immer in Frameworks like Aurelia and Vue due to them not checking for getters/propertyObservers.

The cheapest trick to create a deepcopy would be const newstate = JSON.parse(JSON.stringify(oldstate));

Alternatively you can do spreading not only on top but on all levels of your state.

1 Like

Thanks zewa666 - i’ll try JSON approach as lowest common denominator to rule out other issues. J

1 Like

The JSON approach worked perfectly. Disappointing that Immer won’t work - I really like its approach to immutability and it has a straightforward API too! Thanks for your guidance. J

Edit: incidentally - I found the reference to Immer not working with Aurelia - on discourse here.

1 Like

UPDATE

In case anyone else is in this position, I ended up using Lodash to deep clone my State:

image

All subscriptions to State properties in Aurelia store now working perfectly.

2 Likes

The lodash method has certainly better perf. In general though, going with manual spreads is going to be the most efficient approach alltough the most type-intense :wink:

Since you settled on the deepcloning approach, which frankly for most apps with rather small states won’t really do a noticable perf hit, there is also a convenience approach. You can create a middleware which will create the clone instead and place it with position before. This way you can keep your actions clean and expect always a fresh clone as input.

2 Likes

Hi @jcaddy,

Just be careful with the JSON stringify-parse solution if you have Dates in your data. They will be stringified to strings and parsed back into strings as well. But luckily there is a workaround for that, since the JSON.parse method accepts an optional reviver function.

For clarity I will provide some simple sample code in TypeScript here.

Let’s start with this:

let orig = { created: new Date(2021, 3, 16, 12, 34, 56, 789) };
console.log(orig.created);

This will result in the following console output:

Fri Apr 16 2021 12:34:56 GMT+0200 (Central European Summer Time)

Now let’s make a copy of orig using JSON.stringify() and JSON.parse():

let copy1 = JSON.parse(JSON.stringify(orig));
console.log(copy1.created);

This will actually produce the following console output:

2021-04-16T10:34:56.789Z

To overcome this, let’s create a reviver function and pass it to the JSON.parse() function as well:

function reviver(key: string, value: any) {
  if (key === 'created' && typeof value === 'string') {
    return new Date(value);
  }

  return value;
}

let copy2 = JSON.parse(JSON.stringify(orig), reviver);
console.log(copy2.created);

This will produce the following output:

Fri Apr 16 2021 12:34:56 GMT+0200 (Central European Summer Time)

:sunglasses:

4 Likes