Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Compressing View Logic with Facades
/**
*
* Snippet from blog article: https://auth0.com/blog/ngrx-facades-pros-and-cons/
*
*/
@Component({
selector: 'abl-books-page',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<mat-card>
<mat-card-title>My Collection</mat-card-title>
<mat-card-actions>
<button mat-button raised color="accent" (click)="logout()">Logout</button>
</mat-card-actions>
</mat-card>
<abl-book-preview-list [books]="books$ | async"></abl-book-preview-list>
})
export class BooksPageComponent implements OnInit {
books$: Observable<Book[]>;
constructor(private booksFacade: BooksFacadeService) {
this.books$ = booksFacade.allBooks$;
}
ngOnInit() {
this.booksFacade.dispatch(new BooksPageActions.Load());
}
}
@ThomasBurleson
Copy link
Author

ThomasBurleson commented Jan 16, 2019

Facades have super powers regarding how they encapsulate and use/combine NgRx selectors (aka queries). View components never know about this complexity.

Facades also provide another very important feature: Facades enable view-layer code compression for a super clean, maintainable component implementations.

To code-compress the above example, let's use:

  • inline-initialization of books$
  • call load in constructor (since no input options are used)
  • Move unique action to unique facade method: loadBooksPage()

books-page.component.ts

@Component({
  selector: 'abl-books-page',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `

    <mat-card>
      <mat-card-title>My Collection</mat-card-title>
      <mat-card-actions>
        <button mat-button raised color="accent" (click)="facade.loadBooks()">Reload Books</button>
      </mat-card-actions>
    </mat-card>

    <abl-book-preview-list [books]="books$ | async"></abl-book-preview-list>
})
export class BooksPageComponent implements OnInit {
  books$: Observable<Book[]> = this.facade.allBooks$;

  constructor(private facade: BooksFacadeService) {  }
}

Even better, developers should use ngrx/router and Effects to auto-load BooksPage data when
the BooksPageComponent activates. This would free the view component from load logic also.

Nx DataPersistence makes this super easy. Or just use ROUTER_NAVIGATION:

books.effects.ts

  // Determine if BooksPageComponent is trying to show a book NOT yet loaded
  @Effect()
  autoLoadBook$ = combineLatest(
      this.actions$.pipe(ofType(ROUTER_NAVIGATION)), 
      this.store.pipe(select(BooksQuery.getAllBooks)),
      (action: RouterNavigationAction<RouterState>, books: Book[]) => {
        const bookId = action.payload.routerState.bookId;
        const found = books.find(it => it.bookId === bookId);

       // If specified book is not found in-memory, dispatch action to load it
       return !found ? new LoadBook(bookId) : null;  
      });

@alex-okrushko
Copy link

alex-okrushko commented Jan 17, 2019

export class BooksPageComponent implements OnInit {
  books$: Observable<Book[]> = this.facade.allBooks$;

  constructor(private facade: BooksFacadeService) {  }
}

vs

export class BooksPageComponent implements OnInit {
  books$: Observable<Book[]> = this.store.select(getAllBooks);

  constructor(private store: Store<{}>) {  }
}

How is it any different other than another layer of BooksFacadeService which literally does the same thing.

View components never know about this complexity.

View components never know about the complexity behind selectors as well - they just consume them. Selectors are already the abstraction and encapsulation layer.

@markgoho
Copy link

markgoho commented Jan 17, 2019

I would've done

export class BooksPageComponent {
  constructor(public facade: BooksFacadeService) {  }
}

and

<abl-book-preview-list [books]="facade.allBooks$ | async"></abl-book-preview-list>

Even simpler! Thanks for bringing us this awesome pattern, Thomas!

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