NgRx Select Errors When Attempting Access on Nested Properties

Viewed 810

I'm getting TypeErrors when using NgRx select functions when accessing nested properties.

I have my root store configured in app.module.ts like this:

StoreModule.forRoot({ app: appReducer }),

where app reducer is just a standard reducer. It sets the state correctly; I can see that in the redux dev tools. The selectors for some nested properties that are erroring are:

const getAppFeatureState = createFeatureSelector<IAppState>('app');

export const getAppConfig = createSelector(getAppFeatureState, state => {
  return state.appConfig.data;
});

export const getConfigControls = createSelector(getAppConfig, state => {
  console.log({ state }) // logs values from initial state
  return state.controls;
});

export const getConfigDropdowns = createSelector(
  getConfigControls,
  state => state.dropdowns,
);

When I subscribe to these selectors in app.compontent.ts like this

ngOnInit() {
  this.store.dispatch(new appActions.LoadAppConfig());
  this.store
    .pipe(select(appSelectors.getConfigDropdowns))
    .subscribe(data => {
      console.log('OnInit Dropdowns Data: ', data);
    });
}

app.component.ts:31 ERROR TypeError: Cannot read property 'dropdowns' of null at app.selectors.ts:18

When I add logging to the selectors higher up the chain, I can see that the only elements logged are the initialState values, which are set to null. I don't think this selector function should fire until the value changes from its initial value. But since it doesn't, its unsurprising that I'm getting this error, since it is trying to access a property on null. Is it a necessity that initialState contain the full tree of all potential future nested properties in order not to break my selectors?

How can I prevent this selector firing when its value is unchanged?

Also, Is the StoreModule.forRoot configured correctly? It is somewhat puzzling to me that creating a "root" store, creates the app key in my redux store parallel to my modules' stores, ie, the module stores are not underneath app.

Edit:

Adding general structure of app.reducer.ts. I use immer to shorten boilerplate necessary for updating nested properties, however I have tried this reducer also as the more traditional kind with spread operator all over the place and it works identically.

import produce from 'immer';

export const appReducer = produce(
  (
    draftState: rootStateModels.IAppState = initialState,
    action: AppActions,
  ) => {
    switch (action.type) {
      case AppActionTypes.LoadAppConfig: {
        draftState.appConfig.meta.isLoading = true;
        break;
      }
      /* more cases updating the properties accessed in problematic selectors */
      default: {
        return draftState; // I think this default block is unnecessary based on immer documentation
      }
    }
}

Edit: Add initialState:

const initialState: rootStateModels.IAppState = {
  user: null,
  appConfig: {
    meta: {isError: false, isLoading: false, isSuccess: false},
    data: {
      controls: {
        dropdowns: null,
      }
    },
  },
};
4 Answers

Because you updated your question the answer is https://www.learnrxjs.io/learn-rxjs/operators/filtering/distinctuntilchanged

it allows to emit values only when they have been changed.

store.pipe(
  map(state => state.feature.something),
  distinctUntilChanged(),
)

requires state.feautre.something to have been changed.

The right way would be to use createSelector function that returns memorized selectors that works in the same way as distinctUntilChanged.

You can use filter operator to make sure it emits values only for valid values, and after that you can use pluck operator to emit value of respective nested property.

store.pipe(
  filter(value => state.feature.something),
  pluck('feature', 'something'),
)

The dispatch method is async. So:

ngOnInit() {
  this.store.dispatch(new appActions.LoadAppConfig());
  this.store
    .pipe(select(appSelectors.getConfigDropdowns))
    .subscribe(data => {
      console.log('OnInit Dropdowns Data: ', data);
    });
}

Here the subscription runs faster than the dispatch so the select returns with null value from your initial state. Simply check this in the selector or add initial state. EX:

const getAppFeatureState = createFeatureSelector<IAppState>('app');

export const getAppConfig = createSelector(getAppFeatureState, state => {
  return state.appConfig.data;
});

export const getConfigControls = createSelector(getAppConfig, state => {
  console.log({ state }) // logs values from initial state
  return state.controls;
});

export const getConfigDropdowns = createSelector(
  getConfigControls,
  state => state ? state.dropdown : null,
);

Ok, I took a look again in code and updated my answer.

Can you try below given sample.

this.store
  .pipe(
    // Here `isStarted` will be boolean value which will enable and disable selector. 
    //This can be derived from initial state, if null it wont go to next selector
    switchMap(data => {
      if (isStarted) {
        return never();
      } else {
        return of(data);
      }
    }),
    switchMap(data => select(appSelectors.getConfigDropdowns))
  )
  .subscribe(data => {
    console.log("OnInit Dropdowns Data: ", data);
  });
Related