Angular – Custom RxJS State Manager

Ildar Timerbaev
Webtips
Published in
3 min readOct 1, 2021

Hello there!

All developers will work with a large data in the project. Early or lately simple solutions won’t work and for access or modify data You will have to use the State Management pattern.

Angular Framework has great solutions such as NGRX, NGXS, or another Redux-like package. These packages are wonderful solutions if you have very large data and enough time for writing code. The main problem with these packages is a lot of boilerplate code.

Simple state management

Let’s start with a simple example. We have a movie collection, movie and actor models with computed values for retrieve data, and methods for writing new data.

abstract class Identifiable {
public id: number;
}
class Actor extends Identifiable {
public name: string;
}
class Movie extends Identifiable {
private actors: Map<Identifiable['id'], Actor>;
public title: string;
public setActors(actors: Actor[]): Actor[] {
this.actors = new Map(actors.map(actor => [actor.id, actor]));
}
}
class MovieCollection extends Identifiable {
private movies: Map<Identifiable['id'], Movie>;
public title: string;
public setMovies(movies: Movie[]): Actor[] {
this.movies = new Map(movies.map(actor => [movie.id, movie]));
}
}

Let’s write the service to managing these models:

@Injectable()
export class MovieCollectionService {
public collection: MovieCollection;
}

Reactive State Management issue

Now We can simply retrieve and modify collection in the service methods. As you know Angular Framework perfectly working with Rx.js. It gives us dynamic data streaming: we can modify our data in one place and stream it to all subscribers. So We can update our service and add the ReplaySubject there: it will store an actual copy of the collection and stream to all subscribers.

@Injectable()
export class MovieCollectionService {
private collectionSubject = new ReplaySubject<MovieCollection>(1);
public collection$ = this.collectionSubject.asObservable();
}

It works perfectly with exception of modifying. We should get our collection copy from the subject, modify it and push it to the Subject next. Let’s see how it will looks if we want to set movies to a movie collection.

...
public getCollectionMoviesList(): Observable<any> {
const request$ = this.httpClient.post(...);
return zip(this.collection$, request$).pipe(
tap(([collection, response]) => {
collection.setMovies(response);
this.collectionSubject.next(collection);
});
);
}
...

As you can see our method looks not very obvious because for one HTTP API request we should zip two observables and push the next copy. When your methods will be a little bit more then this boilerplate code will increase fast.

Reactive model with TypeScript Decorators

We can use TypeScript Decorators to solve the problem of boilerplate code with saving models structure. So let’s create a decorator which will make our model reactive: We will extend our class. Need to:

  1. Declare private class property for storing our model snapshot(_stateStorage$);
  2. Describe the private class method for pushing the next updated model snapshot(_updateState);
  3. Register all snapshots getters – assign the snapshot storage observables to them(_snapshots);
export function ReactiveEntity(): ClassDecorator {
return function(target: any) {
const newTarget = target;
newTarget.prototype._stateStorage$ = new ReplaySubject(1);
newTarget.prototype._updateState = function() {
this._stateStorage$.next(this);
}
newTarget.prototype._snapshots.forEach(key => newTarget.prototype[key] = newTarget.prototype._stateStorage$
.asObservable().pipe(
tap
(state => !state ? this._stateStorage$.next(this) : null))
);
return newTarget;
}
}

Now We have to describe snapshots getters. It’s a class property that will be decorated with the Reactive Snapshot decorator and will return Observable of a state. Let’s implement it. In this decorator, We simply register the property as a snapshot getter.

export function ReactiveSnapshot(): PropertyDecorator {
return function(target: any, propertyKey) {
if (!target._snapshots) {
target._snapshots = [];
}
target._snapshots.push(propertyKey);
}
}

Finally need to implement updating of the reactive state in class mutation methods. As You might have guessed it’s a decorator too. We extend the base class method: call the original method before then push the current model state to state storage.

export function UpdateReactiveState(): MethodDecorator {
return function(target: any, propertyKey, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
const result = originalMethod.apply(this, args);
this._updateState();
return result;
}
}
}

Our reactive model was implemented. Let’s see how it looks.

@ReactiveEntity()
class MovieCollection extends Identifiable {
private movies: Map<Identifiable['id'], Movie>;
public title: string;
@ReactiveSnapshot()
public snapshot$: Observable<this>;
@UpdateReactiveState()
public setMovies(movies: Movie[]): Actor[] {
this.movies = new Map(movies.map(actor => [movie.id, movie]));
}
}

Now We can simply update the model and stream all changes with a built-in reactive state.

Support us by following our telegram channel. We post a lot of helpful information about web development every day.

That’s all! Thank you for reading!

--

--