💲 Black Friday Special

Get an extra 35% OFF everything with coupon code BLACK_FRIDAY

0 days
00 hours
00 mins
00 secs

Write Angular like a pro. Angular Icon

Follow the ultimate Angular roadmap.

Custom Reactive Stores with BehaviorSubject

Over the years best practices for state management have solidified in Angular, and there are just so many choices.

NGRX Store and friends are a popular choice, but based on npm downloads there are less than 20% of Angular projects using it.

That means a huge percentage are using other libraries, or simply rolling their own Observables.

If that’s you, or perhaps you’ve been confused by @ngrx/store and want to start with something a bit simpler - this post is for you.

That’s not to say Observables are “simple”, but by understanding how to create your own “reactive store” with Angular, it’ll help make a transition into expanding the idea into your own applications or using a library like NGRX and friends.

Concepts and Usage

Before we dive in, we need to think about the design of a reactive store. I see many developers simply creating Observables here and there, copy/pasting from random StackOverflow posts and piecing something together they don’t quite understand.

How do we want to consume the data once it’s in the store? NGRX provides a select() method that we can use, so let’s go for that:

@Component({...})
export class AppComponent implements OnInit {
  tasks$: Observable<Task[]>;

  constructor(private store: TaskStore) {}

  ngOnInit() {
    this.tasks$ = this.store.select((state) => state.tasks);
  }
}

That’s nice and simple, right? We can pass in a function which returns the state we want from our reactive store.

Let’s set it up.

Model Interfaces and Initial State

First, we’re going to setup an interface or two and create some initialState. This allows us to pass any initial state information into the store and pass it to any components:

interface Task { id: string; title: string, members: string[]; }

interface TaskState { tasks: Task[]; }

const initialState: TaskState = {
  tasks: []
};

Then, we need an @Injectable Service and drop in a BehaviorSubject

Injectable and BehaviorSubject

I like to attach this to a private _state property:

@Injectable({ providedIn: 'root' })
export class TaskStore {
  private _state: BehaviorSubject<TaskState>;

  constructor() {
    this._state = new BehaviorSubject<TaskState>(initialState);
  }
}

BehaviorSubject differs to a Subject in two ways. First, we can pass an initial state value to a BehaviorSubject, whereas with Subject we cannot. Second, BehaviorSubject passes the last value to new subscribers, whereas Subject does not and only notifies on new values.

At this point, the basis for a store is almost there, but where do we introduce an Observable? This is where many developers trip up. Is the BehaviorSubject an Observable? How do I get an Observable?

Strictly speaking, a BehaviorSubject is both an “observer” and type of observable.

If we want to fetch new values from our BehaviorSubject we need to ask for it as an Observable.

For this, I like to use a state$ property with the $ suffix, a naming convention to denote an Observable.

I use an accessor (setters and getters) to allow us to simply reference store.state$ to get our state as an Observable:

@Injectable({ providedIn: 'root' })
export class TaskStore {
  private _state: BehaviorSubject<TaskState>;

  constructor() {
    this._state = new BehaviorSubject<TaskState>(initialState);
  }

  get state$(): Observable<TaskState> {
    return this._state.asObservable();
  }
}

Our component can then consume the store data right away:

@Component({...})
export class AppComponent implements OnInit {
  tasks$: Observable<Task[]>;

  constructor(private store: TaskStore) {}

  ngOnInit() {
    this.tasks$ = this.store.state$.pipe(
      map(state => state.tasks)
    );
  }
}

Obviously this.tasks$ would be passed into an Async Pipe to create the subscription in the template.

You’ll note at this point we have to introduce .pipe(map(...)) after our store.state$ to fetch the property we want.

How about a custom select() method?

Create a select() method

This seems a good place to loop back to create a select() method that accepts a function:

@Injectable({ providedIn: 'root' })
export class TaskStore {
  private _state: BehaviorSubject<TaskState>;

  constructor() {
    this._state = new BehaviorSubject<TaskState>(initialState);
  }

  get state$(): Observable<TaskState> {
    return this._state.asObservable();
  }

  select<K>(selector: (state: TaskState) => K): Observable<K> {
    return this.state$.pipe(
      map(selector),
      distinctUntilChanged()
    );
  }
}

Here we’re using a generic type of K which is the returned state from our selector function. This provides some type-safety features for our method.

Check out what we’ve done so far in the StackBlitz embed:

Create a setState() method

Now time to start setting state in the store.

I prefer to ‘hide’ the implementation details when setting state, which means we can keep things nice and clean for us.

It also means it’s time to start passing new values into our BehaviorSubject.

There’s a few moving parts in this next piece, but have a look and I’ll explain after:

@Injectable({ providedIn: 'root' })
export class TaskStore {
  private _state: BehaviorSubject<TaskState>;

  constructor() {
    this._state = new BehaviorSubject<TaskState>(initialState);
  }

  get state$(): Observable<TaskState> {
    return this._state.asObservable();
  }

  get state(): TaskState {
    return this._state.getValue();
  }

  setState<K extends keyof TaskState, E extends Partial<Pick<TaskState, K>>>(
    fn: (state: TaskState) => E
  ): void {
    const state = fn(this.state);
    this._state.next({ ...this.state, ...state });
  }

  select<K>(selector: (state: TaskState) => K): Observable<K> {
    return this.state$.pipe(map(selector), distinctUntilChanged());
  }
}

First, I’ve added get state() which returns us a state snapshot of the current store value as a plain JavaScript object.

Secondly, our setState() method takes a callback fn which the current state is then passed into - allowing us to call it like this:

this.store.setState((state) => {
  // do something with `state` snapshot
  return {
    // return composed new state here
  };
});

What’s important to note is that this._state.next() is where we pass our new composed state back into the BehaviorSubject, which will notify all subscribers.

Try it by clicking “Add Task”:

At this point we have a fully functional reactive store with a BehaviorSubject, however the store is not very generic and is very closely coupled with our TaskState.

Could we make it more generic?

Abstract Store Class

Let’s introduce a store-level generic type and make our class abstract.

An abstract class means it is never called directly, i.e. new Class(), it is only extended for inheritance purposes.

We can refactor each TaskState to T to act as a generic type and rename a few things, namely Store<T>.

Also note that we want to supply some initialState into the constructor, for this I’ve used the @Inject() decorator which allows us to pass our own data in - instead of an Angular dependency for injection:

@Injectable({ providedIn: 'root' })
export abstract class Store<T> {
  private _state: BehaviorSubject<T>;

  constructor(@Inject('') initialState: T) {
    this._state = new BehaviorSubject<T>(initialState);
  }

  get state$(): Observable<T> {
    return this._state.asObservable();
  }

  get state(): T {
    return this._state.getValue();
  }

  setState<K extends keyof T, E extends Partial<Pick<T, K>>>(
    fn: (state: T) => E
  ): void {
    const state = fn(this.state);
    this._state.next({ ...this.state, ...state });
  }

  select<K>(selector: (state: T) => K): Observable<K> {
    return this.state$.pipe(map(selector), distinctUntilChanged());
  }
}

Now our Store<T> is generic, it means we can create multiple reactive services that simply inherit all this powerful functionality - so we can stop worrying so much about managing BehaviourSubject and all the Observable goodness. It’s hidden!

This is how we’d then use it with a TaskService:

interface TaskState { tasks: Task[]; }

const initialState: TaskState = {
  tasks: []
};

@Injectable({ providedIn: 'root' })
export class TaskService extends Store<TaskState> {
  constructor() {
    super(initialState); // pass initial state
  }

  addTask(task: Task) {
    this.setState((state) => ({
      tasks: [...state.tasks, task]
    }));
  }
}

Our addTask method now can live in the TaskService, holding our business logic for managing the state. You can think of this as an “Action” method, which returns new state much like a “Reducer” would in the Redux paradigm.

Take a look now at our component class, it’s super lean and only communicates outward via the TaskService, creating a nice separation of concern whilst offloading the work to a service.

The benefit of this too is that it can be used in many components and use the same methods:

Each Service we then create can act as an independent Store, inherit our functionality and the sky’s the limit.

@ultimate/lite-store

I actually took this idea further and open sourced a new state management library using this code and a little extra to provide Entity Pattern support, automatic frozen state via Object.freeze() and a few other features like createSelector().

You can check the @ultimate/lite-store’s full source code here and note that most of the code we’ve explored today is readily available for you to use as a neat little package.

If you’re interested in using it, check out the @ultimate/lite-store documentation and get it installed in your project via npm install @ultimate/lite-store. It’s lean, minimal and focused on just being simple. It’s fully typed, tested and ready to go.

I hope you enjoyed creating the Store in this article. We’ve learned a great deal, especially if you’re new to Angular, RxJS an all that comes with it.

🚀 To dive in headfirst and learn how to architect real Angular apps with me, I’d highly recommend checking out my brand new Angular courses.

Happy coding!

Related blogs 🚀

Free eBooks:

Angular Directives In-Depth eBook Cover

JavaScript Array Methods eBook Cover

NestJS Build a RESTful CRUD API eBook Cover