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
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.
Slice of State
The hero state class represents the type definition of the state data required by UI.
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.
The Rxjs map operator exposes it in the component as observable and binds to UI with an Async pipe.
<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.
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.
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.