Http-store with Angular & RxJS

IT-ROCKSTAR ROBIN VAN TIENHOVEN

More and more applications are starting to use the so called ‘Flux pattern’ or ‘state container’. A few libraries that implement this pattern are:

This pattern can save you a lot of bugs and in some cases even aid in debugging your application. This is because they provide immutable objects and, more importantly, unidirectional data flow when it comes to managing your data. No more “strange” modifications that have an effect on various places throughout your application or complex state mangement functions. See for example the facebook notification bug. This is one of the most famous examples that illustrates the reason for Redux’s creation. For more information on Redux see this post.

Should you always use NgRX when implementing Flux?

In my experience, the answer is no. This is mostly because a large application usually has a lot of state management to content with and will therefor use a lot of the library’s features, which makes integrating the library worthwhile. A small application will usually have much less state to manage which makes integrating the library much less worth your while. Adding this library to a small or medium sized application will also create a lot of boilerplate code and adds a lot of unneeded complexity. This in turn makes it harder for new developers to start working on the app.

Are there situations where this pattern comes in handy? Yes! But this doesn’t mean you always have to use a full blown library. One of these situations is managing data from the backend and using this data in multiple places. Thanks to libraries such as RxJS we can do this ourselves!

Getting started

To demonstrate how we can implement Flux in a simpler way we are going to create a small application. This app will receive a list of to-do’s from the backend and will show these to-do’s on multiple places within the application. The tricky part comes when we change one of the to-do’s and all of the places where the to-do is displayed have to be updated accordingly.

Let’s start by creating a new clean Angular application (see the Angular docs for more info on the commands)

ng new http-store --default --minimal

Creating the base store

Since we can have more than one http-store in our application (for example, to-do’s & users) we want to create a base store so that we can reuse the functionality. Create a new Angular service with the following contents. I will go through each section of the code seperately to explain the function of the code (the full service can be found on my gitub):

src/app/service/http-store.service.ts

export interface IdentifiableEntity {
  id: number | string;
}
export abstract class HttpStoreService<T extends IdentifiableEntity> { 

There is only one real requirement on our entities, they must have an ID. This is so that we can keep track of the entities we have and ensures we can edit and remove the intended entity.

/**
 * Our store object, here we store all our fetched entities
 * So we can access them from all parts of our application and have a single source of truth
 */

public readonly entities$: Observable<T[]>; 

What follows is the definition of the entities$ member. This is an RxJS observable. This object is our single source of truth for everyone that decides to listen (subscribe) to our updates. This object holds all of our entities and can only be modified from within the service, making the data flow unidirectional.

/**
 * The reference to the entity subject, through which the entities can be updated.
 * This way we don't expose our `BehaviorSubject` functions to the outside world
 * and limit managing the {@link entities$} to this service only.
 */

private readonly entityRef: BehaviorSubject<T[]>;

protected constructor(protected http: HttpClient) {
  this.entityRef = new BehaviorSubject([]); // A default value of '[]'
  this.entities$ = this.entityRef.asObservable();
} 

The next property is a somewhat strange object, a RxJS BehaviorSubject. This object allows us to provide updates (via .next()) to the entities$ observable. This is private so that we don’t expose the update functionality to the outside world and thus keeping control over the observable. We connect these two properties in the constructor.

private replaceCache(replacements: T[]): void {
  this.entityRef.next(replacements);
}

/**
 * Removes the given entity from the cache
 */

public async removeOneFromCache(toRemove: T): Promise<void> {
  const entities = await this.entities$.pipe(take(1)).toPromise();
  const updated = entities.filter((entity: T) => toRemove.id !== entity.id);

  this.replaceCache(updated);
} 

The two functions further down are the modifiers of the entities$ observable, the so-called reducers.

/**
 * Sends a request to fetch all entities
 */
public getAll(url: string): Observable<void> {
  const request = this.http.get<T[]>(url)
    .pipe(
      map<T[], void>((entities: T[]) => this.replaceCache(entities)), // Replace the cache with the new entities
      shareReplay(1), // We only want to do the request once if the component decides to subscribe on the request
    );

    request.subscribe({
      error: (err) => console.error(err)
    });

    return request;
  }
} 

Below that is a generic get function that makes a call to the backend and updates the entities$ object with the newly retrieved data.

What else can we add?
We can provide much more functionality within our service, like:

  • event messages (like ‘successful get request’);
  • get an item from the cache;
  • update an item in the cache;
  • add an item to the cache;
  • find and item in the cache;
  • Remove an item from the cache;
  • A config object with url’s so that we won’t have to feed it an url for every request we make;
  • and much more.

If we start expanding this service we might even separate the functionality into two different classes, an entity-store, which contains the entities$ object and the reducers and the http-store that extends from the entity-store, which contains all the default calls (getupdateupsertdelete) to the backend.

But for this turorial we’ll keep it to this simple implementation, since it is enough to support our example.

Using the store

Now that we have created our base store, it is time to start using it. Create a second service in the same folder and add the following code:

src/app/service/todo.service.ts

export interface Todo extends IdentifiableEntity {
  id: number;
  title: string;
  completed: boolean;
}

@Injectable({ providedIn: 'root' })
export class TodoService extends HttpStoreService<Todo> {
  constructor(http: HttpClient) {
    super(http);
  }
} 

Since we are using the HttpClient, do not forget to add the HttpClientModule to the imports array of the app.module.ts!

As you can see this service is quite clean and simple. This is because most of the logic we added to the http-store.service.ts is reusable and only needs typing.

Fetching some to-do’s

The next step is to actually fetch some data and display it on the screen. Change the src/app/app.component.ts to the following:

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})

export class AppComponent implements OnInit {
  todos: Observable<Todo[]>; // Our todo reference
  constructor(private todoService: TodoService) {}

  ngOnInit(): void {
    this.todos = this.todoService.entities$; // Assign the todo observable to our local reference
    this.todoService.getAll('https://jsonplaceholder.typicode.com/todos'); // A mock API
  }

  remove(todo: Todo): void {
    this.todoService.removeOneFromCache(todo);
  }
} 

Also change the src/app/app.component.html to display the fetched todo’s:

<ol>
  <li *ngFor="let todo of todos | async">
    {{ todo.title }}
    <button (click)="remove(todo)">remove</button>
  </li>
</ol> 

Notice the | async pipe. This pipe tells Angular to handle our async logic such as subscribeunsubscribe and returning the actual data. If we now run the application (ng serve) and checkout the result at localhost:4200/, we can see a list of 200 to-do’s with a delete button next to each to-do.

If we would press the delete button we can observe the to-do disappearing off of the list. Now this is in and of itself quite nice. But the main power comes from displaying the data in multiple places and updating the data in all of these places. On to the next step!

Showing the data in multiple places

So we can already fetch some to-do’s, store them, display them and manage them. But the next step is using the data in multiple places. Let’s create a new component and let it display the current amount of to-do’s.

src/app/amount.component.ts

@Component({
  selector: 'app-amount-of-todos',
  template: `<div>Amount of todo's: {{ amount }}</div>`
})

export class AmountComponent implements OnInit {
  amount = 0;

  constructor(private todoService: TodoService) {}

  ngOnInit(): void {
    this.todoService.entities$.subscribe((todos) => this.amount = todos.length);
  }
} 

And register this component in the app.module.ts (declarations: [AppComponent, AmountComponent]).
Now in order to use this component we need to reference it from our app.component.html:

<app-amount-of-todos></app-amount-of-todos> <!-- Reference the new component -->

<ol>
  <li *ngFor="let todo of todos | async">
    {{ todo.title }}
    <button (click)="remove(todo)">remove</button>
  </li>
</ol> 

And that’s it! If we now run the application again (or check the recompiled content if you didn’t stop ng serve) we can see a count of the to-do’s at the top and a list of to-do’s beneath it. If you would now delete one of the to-do’s you can see the amount of to-do’s decreasing in both components, without any extra code or state management!

Conclusion

As you can see, a full blow Flux library is not always necessary. We can easily write a small store implementation ourselves that better fits our needs. As this implementation grows and more use-cases start to emerge the use of one of the bigger Flux libraries can always be considered. But until then this small store implementation keeps the application simple while still getting the job done.

You can find the full source code of this example on my github: ngHttp-store