Platform: Strange behavior: adapter.getSelectors() behaves different on different apps

Created on 4 Mar 2019  ยท  15Comments  ยท  Source: ngrx/platform

I've been struggling with a problem since yesterday. I don't know if it's a bug or if I'm doing something wrong. Here is my code and to the end my findings. My code is the same from ngrx-example-app (structure etc) and the only difference is that my state is not loaded lazy . Furthermore im using the latest ngrx version v7.3.

Code:

vehicleClasses.reducer (the same exists for modelSeries.reducer and manufacturer.reducer)

export interface State extends EntityState<VehicleClass> {
  selectedVehicleClassId: string | null;
}
export const adapter: EntityAdapter<VehicleClass> = createEntityAdapter<VehicleClass>({
  selectId: (a: VehicleClass): string => a.uuid,
});
export const initialState: State = adapter.getInitialState({
  selectedVehicleClassId: null,
});

export function reducer(state = initialState, action: VehicleClassesActions): State {
  switch (action.type) {
    case VehicleClassesActionTypes.LOAD_VEHICLE_CLASSES_SUCCESS: {
      return adapter.addAll(action.payload, state);
    }
    default: {
      return state;
    }
  }
}

export const getSelectedVehicleClassId = (state: State) => state.selectedVehicleClassId;

root state tree (vehicleClasses, manufacturer and modelSeries are generated by entity adapter hanging under vehicleFinder)(copied from redux-devtools):

. vehicleFinder
โ”œโ”€โ”€ vehicleClasses
โ”‚   โ”œโ”€โ”€ ids: [...,...,..]
โ”‚   โ””โ”€โ”€ entities: [...,...,..]
โ”‚   โ””โ”€โ”€ selectedVehicleClassId: ...
โ”œโ”€โ”€ manufacturer
โ”‚   โ”œโ”€โ”€ ids: [...,...,..]
โ”‚   โ””โ”€โ”€ entities: [...,...,..]
โ”‚   โ””โ”€โ”€ selectedManufacturerId: ...
โ”œโ”€โ”€ modelSeries
โ”‚   โ”œโ”€โ”€ ids: [...,...,..]
โ”‚   โ””โ”€โ”€ entities: [...,...,..]
โ”‚   โ””โ”€โ”€ selectedVehicleClassId: ...

Configure ActionReducerMap and get entity selectors:

import * as fromVehicleClasses from './vehicle-classes.reducer';
import { ActionReducerMap } from '@ngrx/store';

export const reducers: ActionReducerMap<any, any> = {
  vehicleClasses: fromVehicleClasses.reducer,
  manufacturer: fromManufacturer.reducer,
  modelSeries: fromModelSeries.reducer,
};

export const {
  selectIds,
  selectEntities,
  selectAll,
  selectTotal,
} = fromVehicleClasses.adapter.getSelectors((state: any) => state.vehicleClasses);

module:

StoreModule.forFeature('vehicleFinder', reducers),

When i call fromVehicleClasses.adapter.getSelectors((state: any) => state.vehicleClasses) the state object contains vehicleClasses, manufacturer and modelSeries.

Contrary to my example the ngrx-example-app behaves totally different (it pass the whole global state object) although I don't see any differences to mine except that the corresponding module from ngrx-example-app (books.module.ts) is loaded lazy.

Update: What i forgot to mention is, i have also tried this:

export const selectVehicleFinderState = createFeatureSelector('vehicleFinder');
export const getVehicleClassesEntitiesState = createSelector(
  selectVehicleFinderState,
  state => state.vehicleClasses
);

// @note: this produce an error:
// ERROR TypeError: Cannot read property 'vehicleClasses' of undefined
const result = fromVehicleClasses.adapter.getSelectors(getVehicleClassesEntitiesState);

Code (ngrx-example-app)

books.module.ts

StoreModule.forFeature('books', reducers),

root state tree (search, books and collection hanging under books)(copied from redux-devtools):

. books
โ”œโ”€โ”€ search
โ”‚   โ”œโ”€โ”€ ...
โ”œโ”€โ”€ books
โ”‚   โ”œโ”€โ”€ ids: [...,...,..]
โ”‚   โ””โ”€โ”€ entities: [...,...,..]
โ”‚   โ””โ”€โ”€ selectedBookId: ...
โ”œโ”€โ”€ collection
โ”‚   โ”œโ”€โ”€ ...

https://github.com/ngrx/platform/blob/master/projects/example-app/src/app/books/books.module.ts#L26-L32

books.reducer.ts

https://github.com/ngrx/platform/blob/master/projects/example-app/src/app/books/reducers/books.reducer.ts

books/reducer/index.ts

https://github.com/ngrx/platform/blob/master/projects/example-app/src/app/books/reducers/index.ts
The call on fromBooks.adapter.getSelectors(state => console.log(state)) inside of the ngrx-example-app is logging the whole global state (see below):

. books
โ”œโ”€โ”€ search
โ”‚   โ”œโ”€โ”€ ...
โ”œโ”€โ”€ books
โ”‚   โ”œโ”€โ”€ ids: [...,...,..]
โ”‚   โ””โ”€โ”€ entities: [...,...,..]
โ”‚   โ””โ”€โ”€ selectedBookId: ...
โ”œโ”€โ”€ collection
โ”‚   โ”œโ”€โ”€ ...

Why adapter.getSelectors behaves different? In the ngrx-example-app the argument of the callback function which is called by getSelectors contains the whole global state object and in my app it just contain a slice from a state? What im doing wrong?

The example app has a need for createFeatureSelector but mine needs none because i can not get from top level state because its get some slice state? Why is that so?.

How does the adapter.getSelectors(callbackFunc) work? Will the callbackFunc pass the global state object when calling getSelectors? Why isn't my case like this?

FYI: Unfortunately I cannot provide a repo for moment. I hope this will be enough for now (maybe i can provide later).

Most helpful comment

Cool. Yea, I think the other error was due to the old selector. Glad it got sorted out.

All 15 comments

Can it be that adapter.getSelectors behaves differently with lazy loaded modules? So that it passes the global state object?

@SerkanSipahi IMO the best way to get help is via a complete sample in a StackBlitz or in a GitHub repository,

Thanks @manklu. We already talked to @SerkanSipahi about opening this issue without a full repro yet.

Hmmm, I don't see it directly...

Just to be sure, you did register ngrx/store with StoreModule.forRoot in the AppModule right?

(I've updated the question to add some TS formatting)

@timdeschryver sure.

StoreModule.forRoot({}),

Im having this same issue i Know its not a bug but im missing something in my understanding of working with ngrx enity

Will you show how you are using the selector also? store.pipe(select(...))

export interface State {
  vehicleFinder: VehicleFinderState;
}
export interface VehicleFinderState {
  vehicleClasses: fromVehicleClasses.State;
}
export const reducers: ActionReducerMap<VehicleFinderState> = {
  vehicleClasses: fromVehicleClasses.reducer,
};

export const selectVehicleFinderState = createFeatureSelector<State, VehicleFinderState>(
  'vehicleFinder'
);

export const getVehicleClassesEntitiesState = createSelector(
  selectVehicleFinderState,
  state => state.vehicleClasses
);

export const { selectAll } = fromVehicleClasses.adapter.getSelectors(getVehicleClassesEntitiesState);
export const selectAllVehicleClasses = createSelector(selectVehicleFinderState, selectAll);

// somewhere in the component
...
...
vehicleClasses$: Observable<VehicleClass[]> = this.store.select(selectAllVehicleClasses);
...
...

I think something goes wrong between createFeatureSelector and this.store.select(selectAllVehicleClasses); but I can't exactly localize it!

the compiler also fails.

Error:(56, 55) TS2345: Argument of type 'MemoizedSelector<State, VehicleFinderState>' is not assignable to parameter of type '[SelectorWithProps<{}, {}, State>, SelectorWithProps<{}, {}, {}>, SelectorWithProps<{}, {}, {}>, SelectorWithProps<{}, {}, {}>, SelectorWithProps<{}, {}, {}>, SelectorWithProps<...>, SelectorWithProps<...>, SelectorWithProps<...>]'.
  Type 'MemoizedSelector<State, VehicleFinderState>' is missing the following properties from type '[SelectorWithProps<{}, {}, State>, SelectorWithProps<{}, {}, {}>, SelectorWithProps<{}, {}, {}>, SelectorWithProps<{}, {}, {}>, SelectorWithProps<{}, {}, {}>, SelectorWithProps<...>, SelectorWithProps<...>, SelectorWithProps<...>]': 0, 1, 2, 3, and 33 more.

@brandonroberts i believe you are right with this.store.select(selectAllVehicleClasses);

Yep. The lines that jump out to me are

export const { selectAll } = fromVehicleClasses.adapter.getSelectors(getVehicleClassesEntitiesState);
export const selectAllVehicleClasses = createSelector(selectVehicleFinderState, selectAll);

Your selectAllVehicleClasses is filtering down twice from (root -> all vehicle classes) -> all vehicle classes. Your selector should look something like

export const { selectAll } = fromVehicleClasses.adapter.getSelectors();
export const selectAllVehicleClasses = createSelector(selectVehicleFinderState, selectAll);

vehicleClasses$: Observable<VehicleClass[]> = this.store.select(selectAllVehicleClasses);

OR

export const { selectAll: selectAllVehicleClasses } = fromVehicleClasses.adapter.getSelectors(getVehicleClassesEntitiesState);

vehicleClasses$: Observable<VehicleClass[]> = this.store.select(selectAllVehicleClasses);

When you pass a function to the adapter#getSelectors, you're saying "I'm providing you a selector to get to my entities". The selectAll in this case is a function that returns your ids mapped to your entities. You don't need to define an additional selector.

When you don't pass a function to getSelectors(), you're saying "Just give me a function I will use to compose with another selector"

Thank you, it looks good, the previous mistake will not become more but now i have an other error:

core.js:15723 ERROR TypeError: Cannot read property 'map' of undefined
    at entity.js:28
    at store.js:635
    at memoized (store.js:577)
    at defaultStateFn (store.js:604)
    at store.js:643
    at memoized (store.js:577)
    at MapSubscriber.project (store.js:520)
    at MapSubscriber.push../node_modules/rxjs/_esm5/internal/operators/map.js.MapSubscriber._next (map.js:35)
    at MapSubscriber.push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.next (Subscriber.js:53)
    at MapSubscriber.push../node_modules/rxjs/_esm5/internal/operators/map.js.MapSubscriber._next (map.js:41)

and a compiler error in vehicleClasses$: Observable<VehicleClass[]> = this.store.select(selectAllVehicleClasses);

Error:(18, 67) TS2345: Argument of type '(state: EntityState<VehicleClass>) => VehicleClass[]' is not assignable to parameter of type '"vehicleClasses"'.

I had this map error yesterday (i think i was on a good track but then throw it later because it not working proper for me).

Try this

export const selectVehicleFinderState = createFeatureSelector<State, VehicleFinderState>(
  'vehicleFinder' // feature root
);

export const getVehicleClassesEntitiesState = createSelector(
  selectVehicleFinderState,
  state => state.vehicleClasses // vehicleFinder -> vehicleClasses (ids, entities, selectedVehicleClassId)
);

export const { selectAll } = fromVehicleClasses.adapter.getSelectors(); // select all (map ids to entities)
export const selectAllVehicleClasses = createSelector(getVehicleClassesEntitiesState, selectAll); // vehicleFinder -> vehicleClasses (ids, entities, selectedVehicleClassId) -> selectAll

vehicleClasses$: Observable<VehicleClass[]> = this.store.select(selectAllVehicleClasses);

Yes, its works (renders correct values, no runtime error). Thank you so much. I am infinitely grateful. Now i have to process (brain) the whole thing first :) That was really a lot in the last 2 days for me. Had a lot of pain!

Last think, i get a compiler error for vehicleClasses$: Observable<VehicleClass[]> = this.store.select(selectAllVehicleClasses); compiler error ==> selectAllVehicleClasses <==
The compiler error has no effect when its runs in runtime.

Error:(18, 67) TS2345: Argument of type 'MemoizedSelector<State, VehicleClass[]>' is not assignable to parameter of type '"vehicleClasses"'.

@brandonroberts I think we can close this issue ticket because my last thread has nothing to do with with the initial error (i guess).

Thank you again.

Cool. Yea, I think the other error was due to the old selector. Glad it got sorted out.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Matmo10 picture Matmo10  ยท  3Comments

hccampos picture hccampos  ยท  3Comments

gperdomor picture gperdomor  ยท  3Comments

mappedinn picture mappedinn  ยท  3Comments

axmad22 picture axmad22  ยท  3Comments