A simple but effective solution using observable data service

Angular state management is the core of any Angular App, but there is no one-size-fits-all solution. Although Ngrx is the most popular Angular state management framework, its excessive boilerplate codes make it overkill for many small to middle-size apps.

This article discusses a simple but effective Angular state management pattern using observable data service.

What I want to achieve is to have the following key benefits by using a concept similar to Redux pattern.

  • Single source of truth
  • Unidirectional data flow
  • Immutable state at the component level

A demo Hero application is available as an example of implementing an Angular App with this pattern.

Overview

The diagram below illustrates the data flow of the pattern, which is similar to the Redux flow. The sequence of data flow through the app are:

  • When a user performs an action, an event/action is sent to the store service
  • The store service calls the API service(side effect) if necessary
  • After the API call returns, the store service updates the state
  • The new state is published via Observable steam
  • The component that subscribed to the state re-renders the UI
Redux flow

Store service

In the world of Redux, the store holds the global application state tree, and the state describes the condition of the app at a specific point in time.

In this example app, we use the HeroStore as an observable store service to serve as a store to provide data across the app. And it can be injected into any component where the state is needed.

@Injectable()
export class HeroStore extends Store<HeroState> 

The HeroStore service extends from the abstract Store class. The core of the Store class is Rxjs BehaviorSubject. It is used as a private observable instance to hold the state and emit any change to subscribers.

Please note that a read-only observable state$ is exposed instead of the BehaviorSubject, to enforce the one-way data flow from store service to the components. A setState method is provided as protected; the only way to update the state is from theHeroStore service.

Store

Slice of State

The hero state class represents the type definition of the state data required by UI.

State

Each component in the demo App requires a slice of the state, i.e., the dashBoard component needs the heroes data to show the first five heroes. We expose the slice of state data with read-only observable in-store service.

Obervables

The Rxjs map operator exposes it in the component as observable and binds to UI with an Async pipe.

None
<a *ngFor="let hero of heroes$ | async"><h4>{{hero.name}}</h4></a>

Action Methods

In the HeroStore service, CRUD action methods are exposed to update the state. The following add method is an example.

The add action method triggers an API service call to add the new hero object and update the global state. Please note that the spread operator is used to create a copy of the state before updating the state to ensure immutability.

Add action

The updated state will be published from the store service, all the subscribers of the state data slice will be notified, and UI will be updated. The subscribers don't know which party caused the state change; they have just been notified of the state change. This makes the component decoupled from other parts of the App.

Side effect

When an external API call is made and the state is changed. As a result, a side effect occurs. In Ngrx, the Ngrx effect library is used as a middleware to listen to the actions, trigger the side effects, and return the actions into the reducer.

In this pattern, I want to avoid the complexity of the Ngrx effect, so the action methods trigger the API service calls (side effects), and Rxjs observables are chained to handle the result of those side effects.

The API service is separated into a HeroApiService class to isolate the side effect-related code for maintainability, and the only consumer for the API service is the HeroStore service. In other words, the components cannot call the API service directly; every request must be sent via the store service.

HeroApiService

A few notes

With this pattern, most components become simply dumb and reactive. Firstly, it takes input from the slice of the state, which is the chained/mapped observable from the store service, and binds to HTML with an Async pipe. Then, it handles the user interactions by calling the store service action methods.

When your app grows, you may find the complexity of the state keeps increasing. Then it is probably the time to split your store service into different services by global or feature modules, and you may also make use of Rxjs operators like combinelatest or mergeAll to map/slice the state for the UI to consume.

Conclusion

This observable store service is a simple-to-implement pattern that retains most of the benefits of those more complex state management frameworks. Another benefit is the store service helps to avoid the "event soup" when using too many individual Angular services with observables subscribed everywhere.

Happy programming!

If you are not already a paid member of Medium, you can do so by visiting this link. You'll get unlimited full access to every story on Medium. I'll receive a portion of your membership fees as a referral.