Skip to content

Instantly share code, notes, and snippets.

@ThomasBurleson
Last active February 22, 2024 07:41
Show Gist options
  • Star 44 You must be signed in to star a gist
  • Fork 9 You must be signed in to fork a gist
  • Save ThomasBurleson/38d067abad03b56f1c9caf28ff0f4ebd to your computer and use it in GitHub Desktop.
Save ThomasBurleson/38d067abad03b56f1c9caf28ff0f4ebd to your computer and use it in GitHub Desktop.
Using ngrx with Effects + Facades

NgRx State Management with TicketFacade

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

Solution: API Facade

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$ ));
};
@ThomasBurleson
Copy link
Author

ThomasBurleson commented Aug 8, 2018

Using NgRx Facades with View Components

By Using NgRx Facades, the views no longer worry or even know about actions, reducers, REST services, effects, or even router navigation (when a ticket is selected).

A view component (aka Container) would simply inject and use the 'ticketFacade' as shown below:

ticket-editor.components.ts:
import {Component} from '@angular/core';
import {TicketsFacade} from './+state/tickets.facade';

@Component({
  selector: 'ticket-editor',
  template: `
      <div fxLayout fxLayoutAlign="center center" class="centered" @card>
        <ticket-card  [ticket]="service.selectedTicket$ | async"
                      [users]="service.users$ | async"
                      (save)="service.save($event)"
                      (complete)="service.close($event)"
                      (reassign)="service.assign($event)" >
        </ticket-card>
      </div>
      <a mat-fab title="Add a new ticket" class="floating-button" routerLink="/ticket/new" >
        <mat-icon class="md-24">add</mat-icon>
      </a>
   `
})
export class TicketEditorComponent {
  constructor(public service: TicketsFacade) { }
}

Notice how the presentation ticket-card child component has inputs of raw data (instead of observables).

From the TicketEditorComponent perspective, the facade service black-boxes (i) ALL state management and (ii) delegated user interactions.

Public API

The Facade service provides an explicit public API of:

  • Observable properties: users$, selectedTicket$
  • Public methods: save(), close(), assign()

@nisimjoseph
Copy link

Thank you I love the way it organizes and makes NGRX more sense and order.

@Saber-Bjeoui
Copy link

Thank you for your work, read the article and followed the examples, really nice and clean solutions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment