0

We had the following ngrx reducer, but added a nested object to our state (structure: settings: { a: boolean, b: string }), so decided to use lodash’s cloneDeep(obj) instead of the ...obj spread operator to ensure our state is immutable, as per the second note in the official docs’ “Creating the Reducer Function” section:

The spread operator only does shallow copying and does not handle deeply nested objects. You need to copy each level in the object to ensure immutability. There are libraries that handle deep copying including lodash and immer.

// before:
export function reducer(state: MyState = initialState, action: Actions): MyState {
  switch (action.type) {
    // ...
    case MY_TASK: {
      return {
        ...state,
        prop1: action.payload.prop,
        prop2: initialState.anotherProp
      };
    }
    // ..
  }
}

When we changed that to the following, ngrx kept calling the reducer() method, effectively blocking (crashing) the browser:

// after - causes infinite loop
import * as _ from 'lodash';
// ..

export function reducer(oldState: MyState = initialState, action: Actions): MyState {
  function nextState(stateChanges?: (MyState) => void): MyState {
    const draftState: MyState = _.cloneDeep(oldState);
    if (stateChanges) {
      stateChanges(draftState);
    }
    return draftState;
  }

  switch (action.type) {
    // ...
    case MY_TASK: {
      return nextState(s => {
        s.prop1 = action.payload.prop;
        s.prop2 = initialState.anotherProp;
      });
    }
    // ..
  }
}

stateChangedoesn't even touch the new nested additional state properties (i.e. the settings object), only the flat ones (prop1, prop2) we had previously. settings is, however, part of oldState and thus passed to lodash's cloneDeep().

Any idea why changing the returned state (i.e. using _.deepClone(state) instead of the spread ...state operator) would have that effect - and how to stop it?

We are using @ngrx/entity, so the state is extending EntityState<MyState>. I unserstand that the library's entity functinos return a new state (or "the same state if no changes were made"), and that additional properties like our new nested settings object can be added to the state but that
"[t]hese properties must be updated manually" - which I take to mean I have to clone them myself.

Is the mistake that we attempt to clone the entire state (which extends EntityState<T>), not just the additional properties?

The only changes between the working and the broken code were made in the reducer() method, so I don't think we are accidentally firing any new event we didn't fire before, which would usually associated with causing an infinite loop...

Christian
  • 6,070
  • 11
  • 53
  • 103
  • 3
    It seems like something is dispatching an action whenever a specific part of the state changes. Since you are cloning everything, the state always changes, which causes an action to be dispatched, and so on... – Ori Drori Aug 03 '19 at 14:06
  • I've added breakpoints on every event dispatcher I could find in our code, but so far none fires during that loop. Is there a way to find out what exactly triggers this? – Christian Aug 05 '19 at 08:25

1 Answers1

1

_.cloneDeep is the worst case scenario for ngrx because it means that everything has been touched in the store and it triggers all selectors again.

From the described error I can assume that you have a selector that causes dispatch in some case and because the dispatched action caused _.cloneDeep the selector is triggered again that causes dispatch etc...

satanTime
  • 12,631
  • 1
  • 25
  • 73