@ngrx/entity `Cannot read property 'ids' of undefined` when using together with @ngrx/store? already accounting for custom ids when defining adapter
Asked Answered
G

6

7

I have looked at the 2 other posts regarding this, the one with the wrapping around the larger state does not apply whilst the other one with registering multiple forFeature -- I have done some testing around that and even if I import it before or after the forRoot(reducers) in my app.module, it makes no difference.

I'm sure I'm missing something obvious around the configuration.

My configuration:

export const orderAdapter: EntityAdapter<IOrder> = createEntityAdapter<IOrder>({
  selectId: (order: IOrder) => order._id
});

export interface AllOrdersState extends EntityState<IOrder> {}

export const initialState: AllOrdersState = orderAdapter.getInitialState({
  ids: [],
  entities: {}
});

export const OrdersState = createFeatureSelector<AllOrdersState>('orders');

export const {
  selectIds,
  selectEntities,
  selectAll,
  selectTotal
} = orderAdapter.getSelectors(OrdersState);

My actual reducer:


export function ordersReducer(
  state: AllOrdersState = initialState,
  action: actions.OrdersActions
) {
    case actions.CREATE_MANY: {
      console.log(action.orders);
      return orderAdapter.addMany(action.orders, state);
    }
}

I register in my Order.Module as:

    StoreModule.forFeature('orders', ordersReducer),

My main reducer map:

const reducers: ActionReducerMap<AppState> = {
  order: orderReducer,
  admin: adminReducer,
  tlabs: tlabsReducer,
  reports: reportsReducer,
  settings: settingsReducer,
  orders: ordersReducer
};

Then the main import in app.module:

    StoreModule.forRoot(reducers),

Trying to read the entities:

Constructor:

 private store: Store<AppState>

Actual function:

this.store
      .select(ordersReducer.selectAll)

Where that import is taken from:

import * as ordersReducer from '../../../../../store/reducers/orders.reducer';
Glassful answered 2/10, 2019 at 9:31 Comment(3)
When or where in code do you refer the ` ids`?Lehrer
I mean your ids is really defined when you try to access it?Lehrer
When I export the initial state, the reducer itself works and when I dispatch an action to create the entities, the devtools shows me the data: i.imgur.com/wIxCiC8.png but if I dispatch an action from another reducer, it will remove that data. Selecting it then does not seem to work. EDIT: recording of the state: streamable.com/8jl17Glassful
F
2

It looks like it's trying to read from OrdersState before it has been initialized. Your reducer doesn't actually return the default state in order to initialize it; At least you don't seem to, at the code you present.

Make your reducer return the default state.

export function ordersReducer(
  state: AllOrdersState = initialState,
  action: actions.OrdersActions
) {
    switch(action.type) {
        case actions.CREATE_MANY: {
          console.log(action.orders);
          return orderAdapter.addMany(action.orders, state);
        }

        default: { 
          return state // Here you should return the default state
        }
    }
}
Fatherly answered 2/10, 2019 at 9:54 Comment(1)
this works, I don't 100% understand why though, is it just the way things are initialized? I have indeed forgotten to return a default to my switch case.Glassful
K
6

I had this error because I was setting up my selector with the wrong feature key:

export const selectIocState = createFeatureSelector<fromIoc.IocState>('this_key_was_wrong');

it should match what you have loaded in the feature module:

StoreModule.forFeature('feature_key', iocReducer),
Knossos answered 15/9, 2020 at 13:7 Comment(3)
I had essentially the same error, this post and Brandon Roberts' post in the project helped: github.com/ngrx/platform/issues/932#issuecomment-376022550Coreencorel
I just had the same error, had a lower case letter instead of an uppercase. Thank you for making me double-check it! (Already had spent a hour banging my head against the wall )Brunelleschi
Using string enums for cases like this always helpsCulvert
F
4

I had a similar error to that of yours in entity.js (NGRX implementation file). In my case, I had a lazy-loaded module with its own state that was initially undefined. Since entity selectors don't do any null check prior to grabbing the entity related fields (e.g. ids) it causes this error. My solution was to avoid using orderAdapter.getSelectors(OrdersState) and instead define my own protected selectors which are fairly simple:

export const selectIds = createSelector(selectOrderState, (state) => state?.ids);
export const selectEntities = createSelector(selectOrderState, (state) => state?.entities);
export const selectAll = createSelector(selectOrderState, (state) => (state?.ids as Array<string|number>)?.map(id => state?.entities[id]));
export const selectTotal = createSelector(selectOrderState, (state) => state?.ids?.length);

I don't love the fact that I'm not using the built-in selectors but at least this doesn't break my code. I hope the NGRX team provides a way to avoid situations like this.

Farrago answered 11/5, 2020 at 14:12 Comment(0)
F
2

It looks like it's trying to read from OrdersState before it has been initialized. Your reducer doesn't actually return the default state in order to initialize it; At least you don't seem to, at the code you present.

Make your reducer return the default state.

export function ordersReducer(
  state: AllOrdersState = initialState,
  action: actions.OrdersActions
) {
    switch(action.type) {
        case actions.CREATE_MANY: {
          console.log(action.orders);
          return orderAdapter.addMany(action.orders, state);
        }

        default: { 
          return state // Here you should return the default state
        }
    }
}
Fatherly answered 2/10, 2019 at 9:54 Comment(1)
this works, I don't 100% understand why though, is it just the way things are initialized? I have indeed forgotten to return a default to my switch case.Glassful
R
0

I had the same error, when I forgot to add the "state" as as a second parameter for adapter.addOne(). So in OPs example if the redcuer was written in following way:

      return orderAdapter.addMany(action.orders); // !! lack of "state" as 2nd param

then the error would be exactly the same. In my case this was:

Uncaught TypeError: Cannot read properties of undefined (reading 'ids')
    at Object.operation [as addOne] (entity.js?9b56:59:1)
    at eval (eval at <anonymous> (mapping-fields-group.reducer.ts?fd4f:1:1), <anonymous>:1:17)
    at eval (mapping-fields-group.reducer.ts?fd4f:13:1)
    at eval (store.js?d450:1204:1)
    at combination (store.js?d450:215:1)
    at mappingsDataReducers (mappings-data-state.ts?7c86:21:1)
    at eval (store.js?d450:265:1)
    at combination (store.js?d450:215:1)
    at eval (store.js?d450:254:1)
    at computeNextEntry (store-devtools.js?777a:445:1)
Rupp answered 9/6, 2022 at 10:19 Comment(0)
C
0

For those stumbling into this when running ng test, I was receiving this error because I needed to set up a mock store and mock selectors for testing.

If you are like me and are using an EntityAdapter to streamline your development, then your adapter probably came with a series of selectors such as this:

selectAll: MemoizedSelector<V, T[], (entityState: EntityState<T>) => T[]>;

In that case, the argument of type EntityState that is passed to your selector looks like this:

export interface EntityState<T> {
    ids: string[] | number[];
    entities: Dictionary<T>;
}

So when the selector runs in your code, it expects a state argument to be passed with an ids property. If you do not set up a mock store with mock state to be used for testing, then a state argument of undefined is passed to your selector, resulting in the error:

TypeError: Cannot read properties of undefined (reading 'ids')

Possible Solution

Details on how to set up the testing mocks can be found in the NgRx Testing > Using Mock Selectors documentation.

Here is an example of how I implemented this in my test:

describe('DataManagerService', () => {
  let service: DataManagerService;
  let store: MockStore;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],      
      providers: [
        
        // *** IMPORTANT *** //
        provideMockStore({
          selectors: [{
            selector: myEntityFeature.selectAll,
            value: [{id: 1, name: 'Mock Entity'}],
          };],
        }),
      ],

    });

    service = TestBed.inject(DataManagerService);
    store = TestBed.inject(MockStore); // *** IMPORTANT *** //
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });
});

NOTE: I used the NgRx schematic to generate my entities, actions, reducers, and effects as Features. As such, in the line where I define my selector name myEntityFeature.selectAll, you would need to replace that with a reference to your actual selector. Similarly, the value should be replaced by a mock object matching the interface defined in your selector.

Carpentry answered 23/11, 2023 at 6:3 Comment(0)
G
0

If u use Standalone API, this will solve your problem :

app.config.ts:

import {userReducers} from './store/users/users.reducer';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(withFetch()),
    provideRouter(routes),
    provideStore(),
    provideState({ name: "users", reducer: userReducers }),
    provideEffects([userEffects]),
    provideStoreDevtools({
      maxAge: 25, // Retains last 25 states
      logOnly: !isDevMode(),
      autoPause: true,
      trace: false,
      traceLimit: 75,
      connectInZone: true
    })
  ]
};

user.selectors.ts:

export const adapter: EntityAdapter<User> = createEntityAdapter<User>({
    selectId: (u) => u.id,
    sortComparer: (userA, userB): number => userA.id.localeCompare(userB.id)
});

const selectorFeature = createFeatureSelector<IUsersState>("users");

export const allUsers = createSelector(selectorFeature, adapter.getSelectors().selectAll);
export const countUsers = createSelector(selectorFeature, adapter.getSelectors().selectTotal);

users.state.ts:

export interface IUsersState extends EntityState<User> {
  isLoading: false,
}

users.reducer.ts:

export const userReducers = createReducer(_initialState,
    on(UserActions.get, (state) => ({ ...state, isLoading: true })),
    on(UserActions.getSuccess, (state, action) => adapter.addMany(action.users, { ...state, isLoading: false}))
);
Godwin answered 27/4, 2024 at 12:2 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.