Script Bytes

Tutorials and Tips for Angular, .Net, and More

Custom Angular Data Store

Jeff F
Jeff F
Cover Image for Custom Angular Data Store

Deciding on which data store to use in Angular can be tough. Do you need a large, complex system like NGRX or NGXS, or can you get away with a much more simple system? That decision depends on a lot of factors, but I'm going to show a custom angular data store I've been using for a couple years now which works for most small to medium sized applications and is a great alternative to large tools like NGRX and NGXS.

Bare Bone Store Setup

This data store uses our own service which stores the data in subjects and exposes them as observables. There is a base service that contains all of the base logic and uses generics. This way all we have to do is create a service that extends our base service, tell it which object it is storing, and we have basic CRUD (Create, Read, Update, Delete). I'm going to start with the very basics and expand it out as we add more logic.

The base service has all of the basic CRUD functionality. Here is the bare bones features of a get function to load in a bunch of data. I'll walk through the get functions and add in the post, put, and delete later.

Let's break this down from the top.

protected itemsSubject = new BehaviorSubject<T[]>([]);
items$ = this.itemsSubject.asObservable();

First we declare a protected Behavior Subject that stores an array of type T. This is where generics come in. If you're not familiar with generics, T can be any type of object. It can be a string, a number, or an object.

Then we publicly expose that behavior subject as an observable. Why is the behavior subject protected and the observable public? You don't want the behavior subject public because this service (and services that extend it) should be the only thing putting data INTO the behavior subject. This prevents outside classes from doing a .next() on our behavior subject when they shouldn't be, or pushing bad data through it.

protected get items(): T[] {
  return this.itemsSubject.getValue();
}
protected set items(val: T[]) {
  this.itemsSubject.next([...val]);
}

Next we have a protected getter and setter called items. This is simply a wrapper around our behavior subject so that we can get the current value, or push out new data in a cleaner way. Note that in the setter we use the spread (...) syntax inside our call to next(). This essentially creates a new array, which combined with OnPush change detection can help with performance. To learn more about the spread syntax, check this out.

Then in our constructor we import the angular HtppClient to use to make our http calls and StoreSettings. StoreSettings is a small class to hold...well settings. Here's the whole class:

export class StoreSettings {
  url: string;
  idField: string;
  itemName: string;
}

The only thing we care about right now is url, which is the url to the api endpoint where we get the data.

load() {
  const url = this.settings.url;

  this.http
    .get<T[]>(url)
    .subscribe((response) => {
      this.items = response;
    });
}

And finally, a function called load, which loads our data into the store. It calls http.get, telling it we're going to be getting back an array of T. In the subscribe we use our items setter to push our data into the behavior subject.

Expanding Data Store Functionality

The load function above is pretty bare right now. There are a lot of ways that we can improve on it. For example, how do we know if we made the call but haven't gotten a response yet? What if it fails? Below is my current implementation of my load function. I'll go over most of the new stuff below. I will also have a link to the entire store at the end of the post so you can really dig into it.

That's quite a bit more. Here's a very quick breakdown.

For parameters I have filter, order, page, and pageSize, all optional with default values. Whether you need these depends mostly on the API that you use and what functionality it has. If your api doesn't support any of these, remove them.

The useCache parameter allows us to tell the load function to only load data if we don't already have data. This is useful for data that is used on several pages of your application, but doesn't change and only needs loaded into the application once.

The append parameter allows us to either replace all the data in the store with the new data, or append the new data to the existing data. If you use paging or an infinite scroll for your data, you'll probably want to append your data and not wipe out your previous data.

this.loading = true is another behavior subject with a getter/setter (similar to the items subject/getter/setter). We set it to true before making our http call, and in the finalize() we set it to false so that it will get set to false whether the call is successful or not. This is useful for when you want to show a loading/spinner indicator. Then we set the url with the filter, order, page, and pageSize parameters.

In the pipe we also have a catchError in case our call fails. Catching errors is a big topic, and could be an entire post of its own, but I'm simply passing any errors through a loadError subject.

In the subscribe, we either replace the current items with the new ones, or concat the new items. If we get back no results, we push that through a subject in case any other parts care. This is totally optional.

Putting the Whole Custom Angular Data Store Together

The rest of the store is where the custom angular data store really comes together. It has the remaining CRUD functions, any functions they rely on, and any other subject/getter/setter pairs for alerting of new data or any other actions. Below is the whole store, a service extending it, and a tiny component calling load and displaying the results.

In order to be able to find and replace objects, one of the fields in the StoreSettings model is the idField. This is used in several spots in the store to get data. The idField isn't hard coded because sometimes you pull from different data sources and they might have different standards for naming the id field (ex: id, _id, personId, etc). The idField is the name of the field, and it accesses the property by using bracket notation. Example: object[this.settings.idField] === 123.

Extending the Store Service

The beauty of using a base store service and extending it is that the service extending it appears very small in size and yet has a lot of functionality. This is an example of a service that extends it to store User data from a prototyping api JSONPlaceHolder. The only special thing about extending a class in Typescript is that we need to call super(), which calls the constructor on the parent class. In this case, our store needs an HttpClient and a StoreSetting model, so we inject our own HttpClient, and pass that as a parameter in the call to super along with our our StoreSettings.

That's it! This PersonService now has full CRUD capabilities and all the capabilities of the store.

Note: I did have to remove the page/pageSize/etc parameters on the load call other wise the json placeholder api throws an error.

Using the Person Service

Here is a super-super simple component that lists the people returned and lets you click an item to 'select' it. It is limited in functionality just to show a few examples of how to consume the service.

Final Thoughts

Hopefully this example of a custom angular data store will help you either create your own or expand this to fit your needs! This example definitely has lots of room for improvements, so feel free to take it and make it your own!