Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular & NGRX prevent selector from emitting identical value on state change when values are equal

Tags:

angular

ngrx

I'm searching for a solution to make my selector only emit a new value when the it has changed compared to the last emitted value, and not only the reference to the store is changed.

I have the following state in my store:

{
   items: [],
   loading: false,
   selectedItemId: 1
}

And I have the following selector:

export const getSelectedItem = createSelector(getItemsState,
    (state) => {
        return state.selectedItemId === null ? null : state.items.find(item => item.id === state.selectedItemId)
    }
);

When I do a subscribe on this selector this fires each time when for example the loading flag in the store changes. I would like the selector to emit only a value when the value of the selected item has changed. I.e. from 1 to 2. But not when the reference of the state object is different.

I found one solution for my problem with doing this:

this.itemStore.select(getSelectedItem).distinctUntilChanged((x, y) => {
    return x.id === y.id;
}).subscribe(item => {
    // do something
});

However, I would like to move the logic to make the updated distinct into my selector instead of having this on the calling side.

I understand why I have this behavior as the framework only checks for object equality and the store reference changes as we return a new ItemState object from the reducer each time something changes in there. But I’m unable to find a solution to my problem, and I cannot imagine that I’m the only person in need of a selector which updates only when the effective value has been changed.

The code for the reducer looks like this:

export function itemsReducer(state: ItemsState = initialItemsState, action: Action): ItemsState {
    switch(action.type) {
        case itemActions.ITEMS_LOAD:
            return {
                ...state,
                itemsLoading: true
            };
        case itemActions.ITEMS_LOAD_SUCESS:
            return {
                ...state,
                items: action.payload,
                itemsLoading: false
            };
        // ... a lot of other similar actions
    }
}
like image 437
Chris Avatar asked Aug 22 '18 07:08

Chris


1 Answers

With your current reducer, distinctUntilChanged is the correct operator to handle this. Another way to address this would be by making your reducer detect whether the object is "functionally unchanged" when updating the items, for example, something like:

export function itemsReducer(state: ItemsState = initialItemsState, action: Action): ItemsState {
    switch(action.type) {
        case itemActions.ITEMS_LOAD:
            return {
                ...state,
                itemsLoading: true
            };
        case itemActions.ITEMS_LOAD_SUCESS:
            return {
                ...state,
                items: action.payload.map((newItem) => {
                   let oldItem = state.items.find((i) => i.id == newItem.id);
                   // If the item exists in the old state, and is deeply-equal
                   // to the new item, return the old item so things watching it
                   // know they won't need to update.
                   if (oldItem && itemEqual(newItem, oldItem)) return oldItem;

                   // Otherwise, return the newItem as either there was no old
                   // item, or the newItem is materially different to the old.
                   return newItem;
                }),
                itemsLoading: false
            };
        // ... a lot of other similar actions
    }
}

You would need to implement itemEqual somehow to check for deep logical equality. This is where you use your knowledge of what, functionally, "equal" means to decide whether the item has changed, for example:

function itemEqual(item1, item2) {
    // Need here to do a /deep/ equality check between 
    // the two items, e.g.:
    return (item1.id == item2.id && 
           item1.propertyA == item2.propertyA &&
           item1.propertyB == item2.propertyB)
}

You could use something like lodash's _.isEqual() function instead of implementing your own itemEqual for a general deep equality check on the objects.

like image 70
Mark Hughes Avatar answered Oct 10 '22 09:10

Mark Hughes