codamit.dev

Data store service - Angular

October 19, 2019

Managing state is challenging when building modern web applications.

In this post I will show you a simple way to organize your own state logic using the power of RxJS and Angular services.

How to cook the rabbit

A data store is a service that manage a single data type. It’s responsible of storing, manipulating, and exposing the data to the rest of the application.

@Injectable({ providedIn: 'root' })
export class BookStore {
  /* Internal books state initialized with an empty Array */
  private readonly _books = new BehaviorSubject<Book[]>([]);

  /* Books are exposed as an Observable to avoid doing anything from outside */
  public readonly books$ = this._books.asObservable();
}

We use a BehaviorSubject to store the current state and an Observable to publicly expose data.

The BehaviorSubject holds the value that needs to be shared with other components. These components subscribe to the observable without the ability to change the value.

Now imagine we want to fetch some books from a remote web service.

@Injectable({ providedIn: 'root' })
export class BookService {
  constructor(private http: HttpClient) {}

  getBooks(): Observable<Book[]> {
    return this.http.get('https://books.com/api/books');
  }
}

We can add a communication layer, the BookService that encapsulates HTTP logic.

Back to the BookStore, we can fetch the data and emit a new state using the next function.

@Injectable({ providedIn: 'root' })
export class BookStore {
  private readonly _books = new BehaviorSubject<Book[]>([]);
  public readonly books$ = this._books.asObservable();

  constructor(private bookService: BookService) {}

  /* Fetch books and update the state accordingly */
  getBooks(): Observable<Book[]> {
    return this.bookService
      .getBooks()
      .pipe(tap(books => this._books.next(books))); /* 👈🏼 Update the state */
  }
}

Now the component can use the store to load books at initialization.

@Component({
  selector: "app-root",
  template: `
    <ng-container *ngIf="books$ | async as books">
      <app-book *ngFor="let book of books; trackBy: trackById"></app-book>
    </ng-container>
  `,
})
export class AppComponent implements OnInit {
  readonly books$: Observable<Book[]> = this.bookStore.books$;

  constructor(private bookStore: BookStore) {}

  ngOnInit() {
    this.bookStore
      .getBooks()
      .subscribe({ error: () => /* @todo should probably handle error */ });
  }

  trackById(index: number, book: Book) {
    return book.id;
  }
}

Each time the books$ observable emits a new state, all observers are updated in order, which means that the view will always be synchronized with the state.

@Component({
  selector: 'app-book',
  template: `
    <article>
      {{ book.title | capitalize }}
      <strong>{{ book.author | capitalize }}</strong>
    </article>
  `,
})
export class AppBook implements OnInit {
  @Input() book: Book;

  constructor(private bookStore: BookStore) {}

  ngOnInit() {}
}

Pros

  • Simplicity, we can quickly understand this architecture.
  • Reactivity, we get all the benefices from RxJS to create reactive UIs.
  • Velocity, with this fashion developers are able to rapidly build features without all the boilerplate needed for an immutable store.

Cons

  • Scalability, as the application grow, the number of service increase and it tends to rapidly become a big spaghetti dish.
  • Testability, testing the store is not trivial because it does a lot of different things.
  • Strictness, this pattern doesn’t come with strict rules to ensure a kind of global coherence.
Edouard Bozon

I'm Edouard Bozon, I live in Lyon, France. I play almost everyday with Angular and Node. I focus my work on building better JavaScript apps and contributing to open source. Looking for a passionate developer? Let's talk together.

Twitter profileGithub profileLinkedin profileEmail me