Cannot read property of map of undefined when using featureSelector ngrx

Viewed 6154

REPO: https://github.com/morningharwood/platform/tree/feature/ngrx-firebase

When I try to subscribe to my selectAll of my post ngrx entity feature I get the error:

Cannot read property of map of undefined

It has to be a simple typo I'm following tutorials verbatim.

Weird part is if I use string selector:

 this.selected$ = this.store.select('post');

 this.selected$.subscribe(console.log); // no error here.

I get a no error.

Question: When using ngrx entity, How can I get my selectAll selector to log out the all posts from ngrx store collection post?

My post.reducer.ts

import {
  PostActions,
  PostActionTypes,
} from './post.actions';

import {
  ActionReducerMap,
  createFeatureSelector,
} from '@ngrx/store';
import {
  createEntityAdapter,
  EntityState,
} from '@ngrx/entity';
import { Post } from './post.interfaces';

export const postAdapter = createEntityAdapter<Post>();
export interface PostState extends EntityState<Post> {}

export const postInitialState: PostState = {
  ids: [ '1234' ],
  entities: {
    '1234': {
      id: '1234',
      header: {
        title: 'title 1',
        subtitle: 'subtitle 1',
      },
      body: {
        sections: [],
      },
    },
  },
};

export const initialState: PostState = postAdapter.getInitialState(postInitialState);

export function postReducer(state: PostState = initialState, action: PostActions): PostState {
  switch (action.type) {
    case PostActionTypes.ADD_ONE:
      return postAdapter.addOne(action.post, state);
    default:
      return state;
  }
}

export const getPostState = createFeatureSelector<PostState>('post');
export const {
  selectIds,
  selectEntities,
  selectAll: selectAllPosts,
  selectTotal,
} = postAdapter.getSelectors(getPostState);

export const postReducersMap: ActionReducerMap<any> = {
  post: postReducer,
};

And app.component:

import {
  Component,
  OnInit,
} from '@angular/core';
import { AppState } from './app.interfaces';
import { Store } from '@ngrx/store';
import { selectAllPosts } from './post/post.reducer';

import { Observable } from 'rxjs/Observable';


@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ],
})
export class AppComponent implements OnInit {
  public selected$: Observable<any>;

  constructor(private store: Store<AppState>) {
  }

  ngOnInit() {

    this.selected$ = this.store.select(selectAllPosts);
    this.selected$.subscribe(console.log); // error here
  }
}

My forFeatureand forRoot modules:

@NgModule({
  imports: [
    BrowserModule.withServerTransition({ appId: 'portfolio-app' }),
    StoreModule.forRoot({ reducers }),
    EffectsModule.forRoot([]),
    PostModule,
  ],
  declarations: [ AppComponent ],
  bootstrap: [ AppComponent ],
  providers: [],
})
export class AppModule {
}

import { StoreModule } from '@ngrx/store';
import { postReducers } from './post.reducer';


@NgModule({
  exports: [],
  declarations: [],
  imports: [StoreModule.forFeature('post', postReducersMap)],
})
export class PostModule {
}
4 Answers

For people who come across this in the future this can be solved by refactoring the selectors slightly.

As per the (somewhat incomplete at the time of posting) docs here, ngrx.io/entity/adapter, you need to select the featureSate using the createFeatureSelector function and then use that substate with the adapter selectors to select slices of that state.

Change this:

post.reducer.ts

export const getPostState = createFeatureSelector<PostState>('post');
export const {
  selectIds,
  selectEntities,
  selectAll: selectAllPosts,
  selectTotal,
} = postAdapter.getSelectors(getPostState);

to this:

export const selectPostState = createFeatureSelector<PostState>('post');
export const {
  selectIds,
  selectEntities,
  selectAll
  selectTotal,
} = postAdapter.getSelectors();

export const selectPostIds = createSelector(
  selectPostState,
  selectIds
 );

export const selectPostEntities = createSelector(
  selectPostState,
  selectEntities
);

export const selectAllPosts = createSelector(
  selectPostState,
  selectAll
);

This will select the substate of the postState feature.

Hope this helps

Edit: So it appears the above two examples are equivalent and the issues I was facing was the same as Matthew Harwood and it was improper use of the ActionReducerMap.

I think the docs might be a bit confusing on this as they lead to believe that the state of and entity is the same thing as the state of a feature state such as a lazy loaded state.

While an entity state has properties such as id's and entities, they do not need a reduce each like feature state does but only one reducer to cover all the entity state for that model.

e.g.

export const initialState: PostState = postAdapter.getInitialState(postInitialState);

does not need a reducer map for the PostAdapterState because the one reducer

export function postReducer(state: PostState = initialState, action: 
PostActions): PostState {
  switch (action.type) {
    case PostActionTypes.ADD_ONE:
      return postAdapter.addOne(action.post, state);
    default:
      return state;
  }
}

Covers all the entity state updating.

You would need a reducer map if you multiple adapter states in a feature state such as the example below.

Lets say you had a Library feature state that and entities of books and magazines. It would look something like the below.

book.reducer.ts

export interface BookEntityState extends EntityState<Book> {
  selectedBookId: string | null;
}

const bookAdapter: EntityAdapter<Book> = createEntityAdapter<Book>();

export const initialBookState: BookEntityState = adapter.getInitialState({
  selectedBookId: null
});

export const bookReducer (...) ... etc

magazine.reducer.ts

export interface MagazineEntityState extends EntityState<Magazine> {
  selectedMagazineId: string | null;
}

const magazineAdapter: EntityAdapter<Magazine> = createEntityAdapter<Magazine>();

export const initialMagazineState: MagazineEntityState = 
adapter.getInitialState({
  selectedMagazineId: null
});

export const magazineReducer (...) ... etc

And then you library feature state may be something like

library.reducer.ts

// Feature state,
export interface LibraryFeatureState {
  books: BookEntityState
  magazines: MagazineEntityState
}

// Reducer map of the lirbray
export const libraryReducersMap: ActionReducerMap<LibraryFeatureState> = {
  books: bookReducer,
  magazines: magazineReducer
};

And then adding the feature state to your Store in the LibraryModule imports array.

library.module.ts

...
StoreModule.forFeature<LibraryFeatureState>('libraryState', 
libraryReducersMap),
...

Hope this helps someone else.

I ran into the same problem trying to create a selector with the following

export const selectFuturMenus = createSelector(
  selectMenuState,
  fromMenu.selectAll,
  (state, menus: Menu[], props) => menus.filter(menu => {
    let startMmt = moment(menu.start_date, 'YYYY-MM-DD');
    let endMmt = moment(menu.end_date, 'YYYY-MM-DD');
    let mmt = moment();
    console.log(startMmt, endMmt);
    return mmt.isBefore(endMmt);
  })
);

It seems we can't use an adapter selector before a filter function. Following the NgRX documentation a simple solution is to decompose the selector like this

export const selectAllMenus = createSelector(
  selectMenuState,
  fromMenu.selectAll,
);

export const selectFuturMenus = createSelector(
  selectAllMenus,
  (menus: Menu[], props) => menus.filter(menu => {
    let startMmt = moment(menu.start_date, 'YYYY-MM-DD');
    let endMmt = moment(menu.end_date, 'YYYY-MM-DD');
    let mmt = moment();
    return mmt.isBefore(endMmt);
  })
);

I've been struggling my mind around same issue during a couple of hours and I finally realized that the source of my issue was related with the reduced function, concretely my reducer function was not returning the default state at the end of the switch statement.

As I said it took me a while to find it out and I hope my answer to help anybody else to fix this issue sooner than me.

switch (action.type) {

    case ActionType:
      return adapter.addAll(action.payload, state);

    default:
      return state;
  }

It was the creation of the ActionMapReducer in the forFeature('post', postReducerMap); I still need to figure out how to use entity and ActionMapReducer together.

Related