Search code examples
angularreduxngrxngrx-store

modifying nested object by key do not trigger effect in NGRX


I have an NGRX store looking like this :

export interface INavigationSettings {
  gridLayout: {
    [Breakpoints.Small]: GridLayout;
    [Breakpoints.Large]: GridLayout;
  };
  //...
}

I have an action that will apply modification to those GridLayout

const SET_NAVIGATION_GRID_VISIBILITY = (state: State, action: featureAction.SetNavigationGridVisibility) => {
  state.navigation.gridLayout[action.payload.size].visibility = {
    ...state.navigation.gridLayout[action.payload.size].visibility,
    ...action.payload.visibility
  };
  return state;
};

This change is correctly applied in the store enter image description here

The problem is, my selector selectNavigationGridLayout

export const selectSettingsState: MemoizedSelector<object, State> = createFeatureSelector<State>('settings');

export const gridLayout = (state: State): {
    Small: featureModels.GridLayout;
    Large: featureModels.GridLayout;
} => state.navigation.gridLayout;

export const selectNavigationGridLayout: MemoizedSelector<object, {
    Small: featureModels.GridLayout;
    Large: featureModels.GridLayout;
}> = createSelector(selectSettingsState, gridLayout);

Never catch any changes, and do not call the change state trough the app. It was working fine before when I had a single object gridLayout, but since I am doing mobile, I separated in 2 pieces =>

  gridLayout: {
    [Breakpoints.Small]: GridLayout;
    [Breakpoints.Large]: GridLayout;
  };

and now it never triggers.

I also tried to

return {
   ...state
}

EDIT :

I changed to this

const SET_NAVIGATION_GRID_VISIBILITY = (state: State, action: featureAction.SetNavigationGridVisibility) => {
  return {
    ...state,
    navigation: {
      ...state.navigation,
      gridLayout: {
        ...state.navigation.gridLayout,
        [action.payload.size]: {
          ...state.navigation.gridLayout[action.payload.size],
          visibility: {
            ...state.navigation.gridLayout[action.payload.size].visibility,
            ...action.payload.visibility
          }
        }
      }
    }
  };
};

and it works, but it's terrible, isn't there a better way ?


Solution

  • Your last edit works because you are returning new state, not mutating the existing state.

    Here are some "prettier" solutions to return new state.

    Solution 1: ActionReducerMap

    Another solution is to use ActionReducerMap to decompose your reducers into being focused on a particular piece of state.

    I see that your top-level feature is named settings. So your store looks a little like this:

    interface StoreState {
      settings: SettingsFeatureState;
    }
    
    interface SettingsFeatureState {
      navigation: INavigationSettings;
    }
    
    interface INavigationSettings {
      gridLayout: GridLayoutState;
    }
    
    interface GridLayoutState {
      [Breakpoints.Small]: GridLayout;
      [Breakpoints.Large]: GridLayout;
    }
    

    And your settings reducer looks like one of the two:

    function settingsReducer(state: SettingsFeatureState, action: Action): SettingsFeatureState {
     // ...
    }
    
    // or
    
    function navigationReducer(state: INavigationSettings, action: Action): INavigationSettings {
     // ...
    }
    
    const settingsReducer: ActionReducerMap<SettingsFeatureState> = {
      navigation: navigationReducer
    };
    

    Do the following steps to decompose your state reducers even more.

    Create a grid layout reducer like this:

    function gridLayoutReducer(state: GridLayoutState, action: Action): GridLayoutState {
      // ...
    }
    
    const SET_NAVIGATION_GRID_VISIBILITY = (state: GridLayoutState, action: featureAction.SetNavigationGridVisibility): GridLayoutState => {
      return {
        ...state,
        [action.payload.size]: {
          ...state[action.payload.size],
          visibility: {
            ...state[action.payload.size].visibility,
            ...action.payload.visibility
          }
        }
      };
    };
    

    Then, modify your navigationReducer register the gridLayoutReducer as follows:

    const navigationReducerMap: ActionReducerMap<INavigationSettings> = {
      gridLayout: gridLayoutReducer
    }
    
    // This function has the following signature:
    // navigationReducer(state: INavigationSettings, action: Action): INavigationSettings 
    const navigationReducer = combineReducers(navigationReducerMap)
    

    Solution 2: Clone State

    If you really don't want to return new state, you could keep your original logic with a slight modification using something like lodash to deep clone the state:

    const SET_NAVIGATION_GRID_VISIBILITY = (state: State, action: featureAction.SetNavigationGridVisibility) => {
      const newState = _.deepClone(state)
    
      newState.navigation.gridLayout[action.payload.size].visibility = {
        ...newState.navigation.gridLayout[action.payload.size].visibility,
        ...action.payload.visibility
      };
      return newState;
    };
    

    This returns new state since you've completely cloned the state. This approach will use a lot more resources, however, since you're cloning the entire state just to modify a few properties.