Selectors
Adding a first selector
In this part of the lab, you'll add a selector that queries all the flights that are not on a defined negative list.
-
Open the file
flight-booking.reducer.tsand add a propertynegativeListto yourState:export interface State { flights: Flight[]; negativeList: number[] } export const initialState: State = { flights: [], negativeList: [3] };For the sake of simplicity, this example defines a default value for the negative list to filter the flight with the id 3.
-
In your
+statefolder, create a fileflight-booking.selectors.tsand enter the following lines. If it already exists, update it as follows:import { createSelector } from "@ngrx/store"; import { FlightBookingAppState } from "./flight-booking.reducer"; export const selectFlights = (s: FlightBookingAppState) => s.flightBooking.flights; export const negativeList = (s: FlightBookingAppState) => s.flightBooking.negativeList; export const selectedFilteredFlights = createSelector( selectFlights, negativeList, (flights, negativeList) => flights.filter(f => !negativeList.includes(f.id)) ); -
In your
flight-search.component.ts, use the selector when fetching data from the store:this.flights$ = this.store.select(selectedFilteredFlights); -
Test your application.
Bonus: Using feature selectors *
To get rid of your FlightBookingAppState type, you can use a feature selector pointing to the branch of your feature:
// Create feature selector
export const selectFlightBooking = createFeatureSelector<State>('flightBooking');
// Use feature selector to get data from feature branch
export const selectFlights = createSelector(selectFlightBooking, s => s.flights);
export const negativeList = createSelector(selectFlightBooking, s => s.negativeList);
[...]
Bonus: Using parameterized selectors *
You can pass parameters to a selector by using a factory. This factory returns the selector creator function to select a specific state slice.
-
In your
flight-booking.selectors.tsfile, add the following selector:export const selectFlightsWithParam = (blockedFlights: number[]) => createSelector( selectFlights, (flights) => flights.filter(f => !blockedFlights.includes(f.id)) ); -
Open the file
flight-search.component.tsand fetch data with this selector:this.flights$ = this.store.select(selectFlightsWithParam([3])); -
Test your solution.
Compose complex component selector **
You use more complex selectors that reuse present selectors and compose a customized result that can be used in a concrete use case implemented in one of your smart components.
-
In your
flight-booking.reducer.tsfile, add the following state definition and initial state:export interface State { flights: Flight[]; // NEW: passenger: Record< number, { id: number, name: string, firstName: string }>; bookings: { passengerId: number, flightId: number }[]; user: { name: string, passengerId: number }; }export const initialState: State = { flights: [], // NEW: passenger: { 1: { id: 1, name: 'Smith', firstName: 'Anne' } }, bookings: [ { passengerId: 1, flightId: 3 }, { passengerId: 1, flightId: 5 } ], user: { name: 'anne.smith', passengerId: 1 } }; -
Open the file
flight-booking.selectors.tsand implement all necessary selectors to select the new state properties.Show code
export const selectPassengers = createSelector( selectFlightBookingState, (state) => state.passenger ); export const selectBookings = createSelector( selectFlightBookingState, (state) => state.bookings ); export const selectUser = createSelector( selectFlightBookingState, (state) => state.user ); -
Define a new selector
selectActiveUserFlightsthat returns only those flights that the active user has booked.Show code
export const selectActiveUserFlights = createSelector( // Selectors: selectFlights, selectBookings, selectUser, // Projector: (flights, bookings, user) => { const activeUserPassengerId = user.passengerId; const activeUserFlightIds = bookings .filter(b => b.passengerId === activeUserPassengerId) .map(b => b.flightId); const activeUserFlights = flights .filter(f => activeUserFlightIds.includes(f.id)); return activeUserFlights; } ); -
Try out your new selector
selectActiveUserFlightsby using it in theflight-search.component.ts. -
Test your solution.
Bonus: Define a basic RxJS Operator that selects from the Store **
Using @ngrx Selectors in combination with RxJS is possible too.
-
Define an RxJS operator which uses the
selectFlightsselector and the RxJS map operator to filter the delayed flights only.Show code
export const selectDelayedRxJSOperator = () => pipe( // RxJS operator to select state from store select(selectFlights), // RxJS map operator map(flights => // Array filter function flights.filter(f => f.delayed) ) ); -
Refactor your
flight-search.component.tsso that it uses the new operator. It can be used like any other Operator with thepipe()method. -
Test your solution.
Bonus: Implement a more generic RxJS operator that selects from the Store ***
The previously implemented operator uses a hard-coded selector and filter logic.
-
Try to implement a more generic operator that uses arguments for the selector and filter.
Show code
export const selectItemsByFilter = <T, K>( mapFn: (state: T) => Array<K>, filter: (item: K) => boolean ) => pipe( // RxJS operator to select state from store select(mapFn), // RxJS map operator map(arr => // Array filter function arr.filter(filter) ) ); -
Refactor your
flight-search.component.tsso that it uses theselectItemsByFilteroperator and use a selector that provides flights and a filter logic to deliver delayed flights only.Show code
[…] ngOnInit() { […] this.flights$ = this.store.pipe( fromFlightBooking.selectItemsByFilter( fromFlightBooking.selectFlights, flight => flight.delayed === false ) ); […] } […]