angular - Hur skapar jag en NGRX-selector som kombinerar data från två olika funktionella moduler?


Jag vill göra det här:

export const selectPlacesForCurrentCustomer = createSelector(
   selectAllPlaces,
   selectedCustomerId,
   (places: Place, customer_id: string) => places.filter((place) => place.customer === customer_id)
) 

Men när jag gör detta får jag detta fel:

Argument av typen 'MemoizedSelector' kan inte tilldelas parametern av typen 'Selector'.

Här är en förenklad version av min ansökan som reproducerar problemet. https://stackblitz.com/edit/angular-10-ngrx-issue?file=src/app/reducers/index.ts

Här är en skärmdump av mitt tillstånd. Jag vill kombinera dessa två saker:

**************** Bakgrund ******************

Jag har två separata funktionslager. Varje lager använder NGRX/Entity.

  1. Platser
  2. Kund

Jag skulle vilja skapa en väljare som hämtar data från båda. Här är ett förenklat gränssnitt för båda modellerna:

export interface Place {
_id: string;
customer: string;
...
}

export interface Customer {
_id: string;
}

Kundmodul

Kundfunktionen har flera butiker. Jag importerar denna funktion till min kundmodul på följande sätt:

 StoreModule.forFeature(fromCustomer.customersFeatureKey, fromCustomer.reducers)

Kundfunktionens status ser ut så här:

export interface CustomerState {
    [fromCustomers.customerFeatureKey]: fromCustomers.State; //I'm want pull data from here!!
    [fromPaymentMethods.paymentmethodFeatureKey]: fromPaymentMethods.State;
    [fromInvoices.invoiceFeatureKey]: fromInvoices.State;
    [fromSubscriptions.subscriptionFeatureKey]: fromSubscriptions.State;
}

// This is what I pass to the `forFeature` method in my customer module. 
export function reducers(state: CustomerState | undefined, action: Action) {
  return combineReducers({
    [fromCustomers.customerFeatureKey]: fromCustomers.reducer,
    [fromPaymentMethods.paymentmethodFeatureKey]: fromPaymentMethods.reducer,
    [fromInvoices.invoiceFeatureKey]: fromInvoices.reducer,
    [fromSubscriptions.subscriptionFeatureKey]: fromSubscriptions.reducer
  })(state, action);
}

Om vi undersöker fromCustomers.State-tillståndet, ser det ut så här:

export interface State extends EntityState<Customer> {
  selectedCustomerId: string | null; // --> This is the first piece of data I'm trying to select.
  loaded: boolean;
  loading: boolean;
  processing: boolean;
}
 
export const adapter: EntityAdapter<Customer> = createEntityAdapter<Customer>({
    selectId: customer => customer._id,
    sortComparer: false
});
 
export const initialState: State = adapter.getInitialState({
    selectedCustomerId: null,
    loaded: false,
    loading: false,
    processing: false
});

...

Som vi kan se ovan, här var det jag slutligen ställde in EntityAdapter. Den viktiga delen av data som jag försöker välja från denna funktion är: selectedCustomerId.

Jag har en väljare i den här funktionen som ser ut så här:

export const selectedCustomerId = createSelector(
    selectCustomersEntitiesState,
    (state) => state.selectedCustomerId
)

Placera modul

Nu när vi har sammanfattat kundmodulen, låt oss gå vidare till Platsfunktionen.

Likt kundmodulen importerar jag detta till platsmodulens fil.

Så här ser mitt platsstatus ut:

export interface PlaceState {
    [fromPlaces.placeFeatureKey]: fromPlaces.State;
    //[fromSearch.searchFeatureKey]: fromSearch.State; // This is where you should add search for assignments
}

export interface State extends fromRoot.State {
  [placesFeatureKey]: PlaceState;
}

// This is what I pass to the `forFeature` method in my place module. 
export function reducers(state: PlaceState | undefined, action: Action) {
  return combineReducers({
    [fromPlaces.placeFeatureKey]: fromPlaces.reducer,
  })(state, action);
}

Om vi dyker ner i [fromPlaces.placeFeatureKey] kommer du att se var jag sätter upp EntityAdapter för platsmodellen.

export interface State extends EntityState<Place> {
  selectedPlaceId: string | null;
  loaded: boolean;
  loading: boolean;
  processing: boolean;
}
 
export const adapter: EntityAdapter<Place> = createEntityAdapter<Place>({
    selectId: place => place._id,
    sortComparer: false
});
 
export const initialState: State = adapter.getInitialState({
    selectedPlaceId: null,
    loaded: false,
    loading: false,
    processing: false
});

Jag har en markör för alla platser:

export const {
  selectIds: selectPlaceIds,
  selectEntities: selectPlaceEntities,
  selectAll: selectAllPlaces, // --> This second thing I'm trying to select. 
  selectTotal: selectTotalPlaces,
} = fromPlaces.adapter.getSelectors(selectPlacesEntitiesState);

Väljaren från platsfunktionen som jag är intresserad av är selectAllPlaces.

Jag hoppas att du fortfarande är med mig. **Nu när jag har lagt bakgrunden, skulle jag vilja förklara vad jag hoppas göra, men inte kan lista ut hur.

********** Jag vill göra detta: **********

export const selectPlacesForCurrentCustomer = createSelector(
   selectAllPlaces,
   selectedCustomerId,
   (places: Place, customer_id: string) => places.filter((place) => place.customer === customer_id)
) 

Men när jag gör detta får jag detta fel:

Argument av typen 'MemoizedSelector' kan inte tilldelas parametern av typen 'Selector'.

Svar #1

Felsökning av problem

Låt oss försöka felsöka den här koden där felet kastas

export const selectPlacesForCurrentCustomer = createSelector(
  selectAllPlaces,
  selectedCustomerId,
  (customer_id: string, places: Place[]) =>
    places.filter(place => place.customer === customer_id)
);

Nedan är en skärmdump av IDE som visar att felet kastas i raden selectedCustomerId

I felet märker vi också raden nedan

Egenskapen '[customersFeatureKey]' saknas i typen 'import("/~/src/app/place/reducers/index").State' men krävs i typen 'import("/~/src/app/customer/reducers/index").State'.

Nu låt oss försöka invertera parametrarna

export const selectPlacesForCurrentCustomer = createSelector(
  selectedCustomerId,
  selectAllPlaces,
  (places: Place[], customer_id: string) =>
    places.filter(place => place.customer === customer_id)
);

Nu ser vi att felet inte längre ligger på selectedCustomerId utan nu på selectAllPlaces och felet har förändrats

Property '[placesFeatureKey]' is missing in type 'import("/~/src/app/customer/reducers/index").State' but required in type 'import("/~/src/app/place/reducers/index").State'.

Förklaring

Nedan är definitionen av createSelector()

createSelector(selectors: [Selector<State, S1>, Selector<State, S2>], projector: (s1: S1, s2: S2) => Result): MemoizedSelector<State, Result>

Lägg märke till något, State Selector<State, S1> och Selector<State, S2> har Samma State

Nedan är ett utdrag från dokumentationen för att förklara varför detta fel uppstår

createSelector kan användas för att välja viss data från tillståndet baserat på flera delar av samma tillstånd.

Notera raden flera skivor av samma tillstånd.

Så i ditt fall skickar du två olika State, State från places som har en ''[placesFeatureKey]'' och State från customers som har en ''[customersFeatureKey]'' som är inkompatibla

Lösning

Alternativ 1 - Typomvandla till samma typ State

export const selectPlacesForCurrentCustomer = createSelector(
  selectAllPlaces as MemoizedSelector<State, Place[]>,
  selectedCustomerId as MemoizedSelector<State, string>,
  (places: Place[], customer_id: string) =>
    places?.filter(place => place.customer === customer_id)
);

Se ett exempel på demo av denna lösning

Alternativ 2 - Istället för att skapa en väljare, använd piping i den observerbara strömmen

Du kan helt enkelt använda piping för att kombinera de två strömmarna i komponenten eller en tjänst

  allPlaces$ = this.store.select(selectAllPlaces);
  selectedCustomerId$ = this.store.select(selectedCustomerId);
  selectPlacesForCurrentCustomer$ = combineLatest([
    this.allPlaces$,
    this.selectedCustomerId$
  ]).pipe(
    map(([places, customer_id]) =>
      places.filter(place => place.customer === customer_id)
    )
  );

Se ett exempel på demo av denna lösning

Alternativ 3 - Skapa en väljare som väljer hela tillståndet och extraherar den relevanta funktionen

export const selectPlacesForCurrentCustomer = (state: State) => {
  const places = selectAllPlaces(state as any);
  const customerId = selectedCustomerId(state as any);
  return places.filter(place => place.customer === customerId);
};

Se exempel-demo