Crocsx
Crocsx

Reputation: 7630

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 ?

Upvotes: 1

Views: 378

Answers (1)

Aaron Adrian
Aaron Adrian

Reputation: 529

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.

Upvotes: 1

Related Questions