Ngrx - selector is not emitting new value after store change

Kaloyan Dimitrov picture Kaloyan Dimitrov · Aug 14, 2018 · Viewed 8k times · Source

I have a state that looks something like this:

export interface State {
  modules: Module[];
}

And the Module interface is something like:

export interface Module {
  name: string;
  structure: {
    moduleID: string;
    icon: string;
    ...
  };
  data: [{id: string; value: string; }];
}

The data in the Modules is connected to input fields and combo boxes. When the user changes something in an input field, an action gets dispatched and the store gets updated by the reducer with the new data value for the given data object. I have already verified that the changes happen in the store.

The Reducer is doing the following: ( getModules() just returns the module with that name, and changeElementData() find the element to change and does data.value = value on it)

case fromTitelActions.SET_DATA: {
      const stateCopy = {...state};
      const moduleToChange = getModule(action.payload.nameOfModule, stateCopy.modules);
      action.payload.data.forEach(data => changeElementData(moduleToChange, data.Id, data.value));
      return stateCopy;
    }

I am trying to subscribe to specific data values and detect changes. My selector looks like this:

export const getDataElementValue = (moduleName, elementId) => createSelector(getModules,
     modules => {
        const module = modules.find(m => m.name === moduleName);
        const data = module.data.find( d => d.id === elementId);
        return data.value;
});

Upon subscribing to the selector I get the current value in it, but it never fires again, no matter how many times the reducer updates that particular data object. Any ideas what I am missing? Thanks.

Answer

Mark Hughes picture Mark Hughes · Aug 14, 2018

The problem is likely because your changeElementData function is not creating new objects when updating the property on moduleToChange.

I would guess you have something like this:

function changeElementData(moduleToChange, id, value) {
    moduleToChange.elements.forEach((el) => {
       if (el.id == id) el.value = value;
    });
}

You would need something like this:

case fromTitelActions.SET_DATA: {
    const stateCopy = {...state};
    stateCopy.modules = stateCopy.modules.map((moduleToChange) => {
        if (module.name != action.payload.nameOfModule) return moduleToChange;
        else return action.payload.data.map(data => changeElementData(moduleToChange, data.Id, data.value));
    });
    return stateCopy;
}

function changeElementData(moduleToChange, id, value) {
    let found: boolean = false;
    let newModule = module;
    if (moduleToChange.elements.find((el) => el.id == id)) {
        newModule = {
            ...moduleToChange,
            elements: moduleToChange.elements.map((el) => {
                if (el.id == id) return { ...el, value: value };
                else return el;
            })
        }
    }
    return newModule;
}

When updating a property of an object on the state, you must ALWAYS create a new containing object, because NGRX uses simple object equality to determine if something has changed and fire the relevant observables. If you update a property on an object, the object itself still has the same reference as its previous version, so NGRX will assume it is unchanged.