You don’t always need a state management library or How to build your own simple Store using RxJs

Kristina Brencheva
Geek Culture
Published in
5 min readJun 2, 2021

--

Sometimes, it’s not necessary to add huge store libraries in an Angular application. All we need is RxJs library which provides observables and operators. In the article we will figure out how to create a state management from scratch using Subject and Observables on the example of the recipe finder Angular application.

What’s the problem?

We have several components or, even, modules around the application which use the same data. How to provide and update this data easily? Where should we keep it?

Mr. Store is hurrying to help!

Defining the Store

Let’s create a separate service where we want to save everything we can use in any component of the project. Also, we would like to take any values from our Store and, furthermore, subscribe to any changes.

export interface Store {
recipes: Recipe[];
savedRecipes: Set<Recipe>;
}
@Injectable({
providedIn: 'root',
})
export class StoreService {
private store: BehaviorSubject<Store>;

constructor() {
this.store = new BehaviorSubject<Store>({
recipes: [],
savedRecipes: new Set<Recipe>()
});
}
}

The Store service has private encapsulated store field which is BehaviourSubject. The store subject will keep our data. There are two entities to which we want to have access everywhere described by Store interface (the recipes which the user is looking for and savedRecipes which the user saves taking it from recipes, we will return to it later).

What’s BehaviourSubject? Why do we use it?

BehaviourSubject is a type of Subject which is a hot Observable.

The main feature of using it is that BehaviourSubject always has a value (because it’s initialized with the value) and saves the last one. Therefore, BehaviourSubject always emits the value unlike Subject, even if it’s not updated by next method.

The value can be taken by the method getValue() and updated by the method next(value).

Taking all into account, BehaviourSubject perfectly fits to keep and update our store values. Let’s add methods for it:

setItem(key: keyof Store, value: any) {
this.store.next({
...this.store.getValue(),
[key]: value,
});
}

private getItem(key: keyof Store): any {
return this.store.getValue()[key];
}
  • setItem method saves new values in the Store. It takes key and value: key is a name of the Store entity and value is an entity which will be saved in the Store. We use the next method of BehaviourSubject to save the updated Store via object spread syntax (we take previous values using the method this.store.getValue()).
  • getItem is private method for taking values from the Store at the current moment. The method gets the value by certain key from the Store. We use getValue method of BehaviourSubject to get saved values in our store (which is object<Store>) and get specific one by provided key-parameter. The main idea to use this method for taking initial values for future updates in other methods of the Store. We don’t make it public in case not to mix synchronous and asynchronous data.

We’ve almost done with building our own Store. However, we still don’t have the ability to subscribe to any changes. For this reason we need an observable which can be used for subscription.

Subject is an Observable too and we can subscribe to it as well. In order to incapsulate our Store from providing unpredictable values using ‘next’ method we save Subject’s observable to the new public field.

Let’s create the Observable and the method for subscription to the specific entity of the Store:

export interface Store {
recipes: Recipe[];
savedRecipes: Set<Recipe>;
}
@Injectable({
providedIn: 'root',
})
export class StoreService {
private store: BehaviorSubject<Store>;
store$: Observable<Store>;
constructor() {
this.store = new BehaviorSubject<Store>({
recipes: [],
savedRecipes: new Set<Recipe>()
});
this.store$ = this.store.asObservable();
}
getItem$(key: keyof Store): Observable<any> {
return this.store$
.pipe(
map((store: Store) => store[key]),
distinctUntilChanged(),
);
}
...

There is store$ is handled using the operator map where store[key] is returned from. Also, distinctUntilChanged used here to prevent extra updates where the subscription will be used. The new stream will be passed only if the value is different from previous one using=== comparison by default (object references must match).

It means that we shouldn’t worry about other entities in the store, our subscription will work only with specific field and when it’s changed and ignore other updated values. Indeed, it’s the useful operator.

Usage

I have built the application which helps users look for recipes by a query and save some of them. I have used edamam.com api for the searching the recipes. The full code you can find on my GitHub page.

The application has two tabs for showing the search field with found recipes and saved recipes.

The user writes the search query and uses filters to set their preferences for the recipes on the first tab. When it is submitted we call getRecipes method in the SearchForm Component which injects Recipe Service. We fetch recipes and then save the result to the Store there:

getRecipes(searchParams: SearchRecipesParams): Observable<Recipe[]> {
this.store.setItem('recipes', null);

return this.apiService.getRecipes(searchParams)
.pipe(
tap((recipes: Recipe[]) => {
if (!recipes.length) {
throw new Error('Nothing found');
}
}),
tap((recipes: Recipe[]) => this.store.setItem('recipes', recipes)),
);
}

We show found recipes through async pipe in Recipes Component. We take it from the Store like Observable using getItem$ method:

<ul class="recipes">
<li *ngFor="let recipe of recipes$ | async">
<app-recipe [recipe]="recipe"
[isSaved]="isSaved(recipe)"
(toggleRecipe)="toggleRecipe($event)"></app-recipe>
</li>
</ul>

At the same time, we have opportunity to save some of recipes there for the second tab. We move specific logic for saving (or deleting) a recipe to the Store and only call the method in components.

toggleSavedRecipe(recipe: Recipe) {
const savedRecipes = this.getItem('savedRecipes');
savedRecipes.has(recipe)
? savedRecipes.delete(recipe)
: savedRecipes.add(recipe);
this.setItem('savedRecipes', savedRecipes);
}

We create a new method in the Store which updates our savedRecipes. We take previous savedRecipes as initial values. When the recipe is chosen, we check whether it’s already saved or not and, depends on the result, we delete or add the recipe in savedRecipes and update the value in the Store using setItem method.

Finally, we show savedRecipes on the second tab, taking values from the Store, and toggle recipes as the same as in the first tab.

That’s it!

I would be happy to answer your questions and hear any feedback from you :)

--

--