Angular NGRX error while querying the store : TypeError: Cannot read property 'map' of undefined

1.5k Views Asked by At

I have implemented NGRX effects in my angular app and get the error below. I am not sure if I am using the selector correctly in my component to query the store ?

core.js:6162 ERROR TypeError: Cannot read property 'map' of undefined
  at ngrx-entity.js:21
    at ngrx-store.js:1198
    at memoized (ngrx-store.js:1039)
    at defaultStateFn (ngrx-store.js:1079)
    at ngrx-store.js:1207
    at memoized (ngrx-store.js:1039)
    at ngrx-store.js:1078
    at Array.map (<anonymous>)

Component

ngOnInit() {
    this.initializeForModel();
    //this.users$ = this.store.select(getAllUsers);

    this.store.select(getAllUsers).pipe().subscribe((response:any)=> {
            this.listOfUser = response.users;
            this.userMange = this._createGroup();
        
      });
      
  

Selector

export const userFeatureSelector = createFeatureSelector<UserState>('users');

        export const getAllUsers = createSelector(
          userFeatureSelector,
          selectAll,
          selectIds
        );

Service

  constructor(private http: HttpClient) { }
          getAllUsers(): Observable<any> {
          return this.http.get("assets/user.json");
          }
        }

Reducer

  import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity';
           
           export interface UserState extends EntityState<User> {
      usersLoaded: boolean;
    }

export const adapter: EntityAdapter<User> = createEntityAdapter<User>();

export const initialState  = adapter.getInitialState({
  usersLoaded: false,
  users: [
  
  ],

});

export interface UserState {
  users: User[];
}
        
       export const userReducer = createReducer(
      initialState,
      on(UserActions.loadUsers, (state, { users }) => {
        return adapter.setAll(users, state);
      }),
      

  
  

Action

export const loadUsers = createAction(
      '[Users List] Load Users via Service',
      props<{ users: User[] }>()
    );

Effects

loadUsers$ = createEffect(() =>
    this.actions$.pipe(
      ofType(UserActions.loadUsers),
     mergeMap(() => this.UserService.getAllUsers()
     .pipe(
       map(users => ({ type: UserActions.loadUsers.type, payload: users }))
     // map(users =>  { {console.log(users)} return UserActions.usersLoaded({users})})()
    ))
    )
  );
        
3

There are 3 best solutions below

1
On

you need to import the map in Effects from rxjs/operators Like this.

import { map, mergeMap, catchError } from "rxjs/operators";

7
On

Don't you miss the return value from selector? And also, effect should return an action which your seems to be not. Don't you get some error also there? If you don't want it, add { dispatch: false} as the second parameter to createEffect() function.

export const getAllUsers = createSelector(
  userFeatureSelector,
  selectAll,
  selectIds
  (feature, all, ids) => ({ feature, all, ids }) // In VS Code selector is underlined as faulty without this
);

EDIT: Try also dispatching an action from the effect or marking it as an effect without dispatch

loadUsers$ = createEffect(() =>
  this.actions$.pipe(
    ofType(UserActions.loadUsers),
    mergeMap(() => this.UserService.getAllUsers()
      .pipe(
        map(users => ({ type: UserActions.loadUsers.type, payload: users }))
      ))
    ),
    //map(mappedUsers => this.store.dispatch(UserActions.saveUser({mappedUsers}))) // - Either dispatch an action to save mapped users
    { dispatch: false} // or add this param so that this effect does not need to dispatch new action
  );

I don't work with NgRx entity/data that often so I'm not exactly sure how it works, but generally, reducer happens before effect. So if you expect in your loadUsers reducer to have data that are supposed to come from BE then you are mistaken. It should be actually an action without the reducer and if the BE call succeeds you can dispatch new action (like `saveUsers) which has the reducer and this one will save your data.

EDIT 2: I think I understood what were you trying to achieve. You were hoping to modify the payload of the loadUsers action with effect, correct? Try this approach:

// actions
export const loadUsers = createAction('[Users List] Load Users via Service'); // empty 
export const saveUsers = createAction('[Users Effect] Save Users', props<{ users: User[] }>());

// reducer
on(UserActions.saveUsers, (state, { users }) => {
  return adapter.setAll(users, state);
}),

// effects
loadUsers$ = createEffect(() =>
  this.actions$.pipe(
    ofType(UserActions.loadUsers),
    concatMap(() => this.UserService.getAllUsers()),
    map(users => UserActions.saveUsers({ users }))       
  ));

If I understand it correctly, the error comes from NgRx Entity and it happens because your reducer for loadUsers has undefined payload. What you tried to to in the effect is not actually updating the payload as the effect happens only after the reducer.

2
On

I was not able to reproduce the exact error you were facing but was able to get the store to work.

Simply the structure of a store is like below

  1. Dispatch an action

Here I changed the action you were initially dispatching to an action without a payload

Now in my actions file I create two actions

import { createAction, props } from "@ngrx/store";
import { User } from "../reducers/user.reducer";

export const loadUsers = createAction(
  "[Users List] Load Users"
);

export const loadUsersSuccess = createAction(
  "[Users List] Load Users Success",
  props<{ users: User[] }>()
);

And in the component we would have

this.store.dispatch(loadUsers());
  1. Effect receives the action and dispatches another action

After we dispatch an action, the effect will receive this action. We will need to call the service and once we receive data dispatch a new action with the payload

@Injectable()
export class UserEffects {
  loadUsers$ = createEffect(() =>
    this.actions$.pipe(
      ofType(UserActions.loadUsers),
      mergeMap(() => this.UserService.getAllUsers()),
      map(users => UserActions.loadUsersSuccess({users}))
    )
  );

  constructor(private actions$: Actions, private UserService: MyService) {}
}

  1. Reducer receives the action and saves data in store

export const userReducer = createReducer(
  initialState,
  on(UserActions.loadUsers, state => {
    return adapter.setAll(state.users, state);
  }),
  on(UserActions.loadUsersSuccess, (state, { users }) => {
    return adapter.setAll(users, state);
  })
);

See this sample stackblitz demo