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.

  1. Open the file flight-booking.reducer.ts and add a property negativeList to your State:

    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.

  2. In your +state folder, create a file flight-booking.selectors.ts and 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))
    );
    
  3. In your flight-search.component.ts, use the selector when fetching data from the store:

    this.flights$ = this.store.select(selectedFilteredFlights);
    
  4. 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.

  1. In your flight-booking.selectors.ts file, add the following selector:

    export const selectFlightsWithParam = (blockedFlights: number[]) => createSelector(
        selectFlights,
        (flights) => flights.filter(f => !blockedFlights.includes(f.id))
    );
    
  2. Open the file flight-search.component.ts and fetch data with this selector:

    this.flights$ = this.store.select(selectFlightsWithParam([3]));
    
  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.

  1. In your flight-booking.reducer.ts file, 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 }
    };
    
  2. Open the file flight-booking.selectors.ts and 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
    );
    

  3. Define a new selector selectActiveUserFlights that 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;
        }
    );
    

  4. Try out your new selector selectActiveUserFlights by using it in the flight-search.component.ts.

  5. Test your solution.

Bonus: Define a basic RxJS Operator that selects from the Store **

Using @ngrx Selectors in combination with RxJS is possible too.

  1. Define an RxJS operator which uses the selectFlights selector 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)
            )   
        );
    

  2. Refactor your flight-search.component.ts so that it uses the new operator. It can be used like any other Operator with the pipe() method.

  3. 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.

  1. 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)
            )
        );
    

  2. Refactor your flight-search.component.ts so that it uses the selectItemsByFilter operator 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
            )
        );
        […]
    }
    […]