Facades are a programming pattern in which a simpler public interface is provided to mask a composition of internal, more-complex, component usages.
When writing a lot of NgRx code - as many enterprises do - developers quickly accumulate large collections of actions and selectors classes. These classes are used to dispatch and query [respectively] the NgRx Store.
Using a Facade - to wrap and blackbox NgRx - simplifies accessing and modifying your NgRx state by masking internal all interactions with the Store, actions, reducers, selectors, and effects.
For more introduction, see Better State Management with Ngrx Facades
Using a Facade pattern to expose a clear, concise services API and encapsulate the use of Effects and NgRx is a wonderful solution:
import { Injectable } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Store } from '@ngrx/store';
import { Actions, Effect, ROOT_EFFECTS_INIT } from '@ngrx/effects';
import { Observable, forkJoin, of } from 'rxjs';
import { map, switchMap, combineLatest, withLatestFrom, mergeMap, concat, merge } from 'rxjs/operators';
import { BackendService } from '../services';
import { NoopAction, ApplicationState } from '../app/+state';
import { LoadAllUsersAction, UsersFacade } from '../users/+state';
import { Ticket, User } from '../ticket';
import { TicketAction, TicketActionTypes, TicketSelectedAction } from './tickets.actions';
import { TicketsQuery } from './tickets.reducers';
import {
FilterTicketsAction,
LoadAllTicketsAction,
SaveTicketAction,
AssignUserAction,
CompleteTicketAction,
TicketsLoadedAction,
TicketSavedAction
} from './tickets.actions';
@Injectable()
export class TicketsFacade {
/**
* Public properties to centralize and expose store queries
*/
users$ = this.users.allUsers$;
filteredTickets$ = this.store.select(TicketsQuery.getTickets);
selectedTicket$ = this.store.select(TicketsQuery.getSelectedTicket);
constructor(
private actions$: Actions,
private store: Store<ApplicationState>,
private users: UsersFacade,
private backend: BackendService,
private route: ActivatedRoute) {
makeTicketID$(route).subscribe(ticketId => {
this.select(ticketId);
});
}
// ***************************************************************
// Public API (normally seen in `ticket.service.ts`)
//
// Except these do not return anything... this only dispatch store actions.
// Use the public observables above to watch for results/changes
//
// ***************************************************************
loadAll() { this.store.dispatch(new LoadAllTicketsAction()); }
select(ticketId:string) { this.store.dispatch(new TicketSelectedAction(ticketId)); }
add(title:string) { this.store.dispatch(new SaveTicketAction({title})); }
close(ticket:Ticket) { this.store.dispatch(new CompleteTicketAction(ticket)); }
save(ticket:Ticket) { this.store.dispatch(new SaveTicketAction(ticket)); }
assign(ticket:Ticket) { this.store.dispatch(new AssignUserAction(ticket)); }
// ***************************************************************
// Public API
// Only one that returns the filteredList (that will be async populated)
// ***************************************************************
getTickets(): Observable<Ticket[]> {
this.loadAll();
return this.filteredTickets$;
}
// ***************************************************************
// Private Queries
// ***************************************************************
private loaded$ = this.store.select(TicketsQuery.getLoaded);
private allTickets$ = this.store.select(TicketsQuery.getAllTickets);
// ***************************************************************
// Effect Models
//
// These are run in the 'background' and are never exposed/used
// by the view components
//
// ***************************************************************
@Effect()
autoLoadAllEffect$ = this.actions$
.ofType(ROOT_EFFECTS_INIT)
.pipe(
mergeMap(_ => [
new LoadAllUsersAction(),
new LoadAllTicketsAction()
])
);
@Effect()
loadAllEffect$ = this.actions$
.ofType(TicketActionTypes.LOADALL)
.pipe(
withLatestFrom(this.loaded$),
switchMap(([_, loaded]) => {
return loaded ? of(null) : this.backend.tickets();
}),
map((tickets: Ticket[] | null) => {
if (tickets) {
tickets = this.users.updateWithAvatars(tickets);
return new TicketsLoadedAction(tickets);
}
return new NoopAction();
})
);
@Effect()
saveEffect$ = this.actions$
.ofType(TicketActionTypes.SAVE)
.pipe(
map(toTicket),
map((ticket: Ticket) => new TicketSavedAction(ticket))
);
@Effect()
completeEffect$ = this.actions$
.ofType(TicketActionTypes.COMPLETE)
.pipe(
map(toTicket),
switchMap(ticket => this.backend.complete(ticket.id, true)),
map((ticket: Ticket) => new TicketSavedAction(ticket))
);
@Effect()
addNewEffect$ = this.actions$
.ofType(TicketActionTypes.CREATE)
.pipe(
map(toTicket),
switchMap(ticket => this.backend.newTicket(ticket)),
map((ticket: Ticket) => new TicketSavedAction(ticket))
);
@Effect()
assignEffect$ = this.actions$
.ofType(TicketActionTypes.ASSIGN)
.pipe(
map(toTicket),
switchMap(({ id, assigneeId }) => this.backend.assign(id, assigneeId)),
map((ticket: Ticket) => new TicketSavedAction(ticket))
);
}
const toTicket = (action: TicketAction): Ticket => action.data as Ticket;
const ofType = (type: string) => (action): boolean => action.type == type;
/**
* For the current route and for future route changes, prepare an Observable to the route
* params ticket 'id'
*/
const makeTicketID$ = ( route: ActivatedRoute ):Observable<string> => {
const current$ = of(route.snapshot.paramMap.get('id'));
const future$ = route.params.pipe( map( params => params['id']));
return current$.pipe(merge( future$ ));
};