How to get Observable that emits only when a property value changes in an array of objects in redux state?

2k views Asked by At

I have redux state in angular app:

export interface UIAction {
  name: string;
  isActive: boolean;
}
    
export interface IUIState {
  actions: UIAction[];
}

I have built an angular service that subscribes to the state and stores Subjects for each action in a Map and returns that subject for callers that are interested in isActive-value for a certain action. It's something like this:

public getIsActive$(name: string): Observable<boolean> {  
  let sub = this.subsMap.get(name);
  return sub.asObservable();
}

Now I want to change the code and get rid of the Map with all the Subjects and do something like this:

public getIsActive$(name: string): Observable<boolean> {  
  return this.ngRedux.select<IUIState>('ui').pipe(
  /* this should use some operators so that
     the returned Observable emits only when 
     UIAction.isActive changes for the action 
     in question. The action in question is
     found from the state.actions-array using 
     parameter 'name' */
   )));
}

How this should be done? Below an example.

Initially the UIState.actions looks like this:

[ { name: 'Save', isActive: false }, { name: 'Cancel', isActive: true } ]

At this point I call

myService.getIsActive$('Save').subscribe(isActive => {
  console.log('Value : ${isActive});
});

then I dispatch a redux state change (only new values here):

{ name: 'Cancel', isActive: false }

The UIState.actions changes to:

[ { name: 'Save', isActive: false }, { name: 'Cancel', isActive: false} ]

The subscription however does not write to console because it wasn't subscribed to changes of 'Cancel' action.

Then I dispatch another redux state change (only values here):

{ name: 'Save', isActive: true }

And the UIState.actions changes to:

[ { name: 'Save', isActive: true}, { name: 'Cancel', isActive: false} ]

Now the subscription writes to console:

Value: true

because it was subscribed to changes of 'Save' action.

2

There are 2 answers

0
char m On BEST ANSWER

If someone is interested in this, I did it like this. Thanks for others pointing me to right direction:

public getIsActive$(name: UIActionName): Observable<boolean> {
  return this.getUIAction$(name).pipe(
    map(axn => axn.isActive)).pipe(distinctUntilChanged());
}

private getUIAction$(name: UIActionName): Observable<UIAction> {
  return this.ngRedux.select<IUIState>('ui').pipe(map(state => {
    return state.actions.find(actn => actn.name === name);
  })).pipe(
    filter((axn: UIAction | undefined): axn is UIAction => axn !== undefined));
}
2
Joshua McCarthy On

Hopefully I'm understanding this requirement correctly. You can apply a custom function to distinctUntilChanged() for more complex cases.

public getIsActive$(name: string):Observable<boolean> {
  return this.ngRedux.select<IUIState>('ui').pipe(
    map(uiState=>uiState.find(state=>state.name===name)),
    filter(state=>!!state),
    distinctUntilChanged((prev, curr)=>
      prev.name===curr.name && prev.isActive === curr.isActive
    ),
    map(state=>state.isActive)
  );
}

Here's a line-by-line of what's going on:

  • Find the state inside the array based on its name
  • Filter out in case .find() returns undefined
  • Apply a custom function that checks both name and isActive. If either property doesn't match its previous value, it will emit the new value.
  • Now that we have our new value, return the isActive boolean.