Hi,
In some cases, with Angular 2+ or any similar library or framework, using components inputs and outputs to share data doesn’t answer all use cases.
This mainly happens when you have two distinct and unrelated Angular 2+ components that manipulate the same data (Ex.: Session, Current user, Cart, User Settings, A component’s state like a bottomsheet or sidenav etc…).
You may want to use ngrx or any redux-like pattern but you will still have to handle all the logic to persist data on your API or anywhere else.
In order to handle communication between components, I use a special pattern I call “Reactive Stores“. It is a simple Angular 2+ service using rxjs‘s ReplaySubject to propagate changes.
First, we need a model:
export class UserSchema { id?: string; firstName?: string; lastName?: string; constructor(args: UserSchema = {}) { this.id = args.id; this.firstName = args.firstName; this.lastName = args.lastName; } } export class User extends UserSchema { /* Methods go here. */ getName() { return `${this.firstName} ${this.lastName}`; } }
I love using this pattern for my models. It might look tricky but it’s simply a way to implement named parameters in TypeScript.
The model’s code is splitted into two classes UserSchema
containing the properties and the constructor then User
containing the helper methods if needed.
The funny thing with this is that we just won a constructor that we can directly use with data we receive from the API or anywhere else new User(data)
and in addition to this, we just won a copy constructor new User(new User())
. This all works thanks to TypeScript’s duck typing.
Now, we need the service:
@Injectable() export class UserCurrentStore { private static _RESOURCE_PATH = '/users'; private _user$ = new BehaviorSubject<User>(new User()); constructor(private _httpClient: HttpClient) { } get user$() { /* We don't want to return our replay subject * because this service should be the only one able to emit new values. */ return this._user$.asObservable(); } updateUser({user}: {user: User}): Observable<User> { return this._httpClient.patch(`${UserCurrentStore._RESOURCE_PATH}/${encodeURIComponent(user.id)}`, user) .map((userData) => this._dataToUser(userData)) /* Propagate the new user to the subscribed components. */ .do((user) => this._updateUser(user)); } private _dataToUser(userData) { return new User(userData); } private _updateUser(user: User) { this._user$.next(user); } }
The cool thing here is the do
rxjs operator in updateUser
. updateUser
will simply return the observable that updates the resource on the API so if something goes wrong, it’s up to the component who called the method to decide how to deal with that error.
If the call succeeds, our arrow function we gave to the do
operator will be called and we will propagate the new user
value using the BehaviorSubject
.
So how does this work?
A BehaviorSubject
is some kind of buffer. Every value we emit using the next
method will be kept in memory so even if a component (or anything else) subscribes to the replay subject, it will receive the whole stream of data we emitted. It’s just like a twitter feed, when you sign in, you see all the previous tweets and anytime there’s a new tweet it’s added on top so you never miss anything.
Let’s see how the components will use this Reactive Store
Here’s how a components updates the state of the store:
@Component({ selector: 'wt-user-signin', template: ` <form> ... </form> ` }) export class UserSignComponent { constructor(private _userCurrentStore: UserCurrentStore) { } onUserSignIn({user}: {user: User}) { this._userCurrentStore.updateUser({user: user}) .subscribe( (user) => console.log('HURRAY!'), (error) => console.error('OUPS! Something went wrong.') ); } }
When we call updateUser
, we get an observable so we candle handle the success and failure as we wish.
And here’s how it is consumed by another component:
@Component({ selector: 'wt-user-preview', template: ` <div>{{ user.firstName }}</div> ` }) export class UserPreviewComponent implements OnInit, OnDestroy { user: User; private _subscription: Subscription = null; constructor(private _userCurrentStore: UserCurrentStore) { } ngOnInit() { this._subscription = this._userCurrentStore.user$ .subscribe(user => this.user = user); } ngOnDestroy() { if (this._subscription !== null) { this._subscription.unsubscribe(); } } }
We subscribe to the user$
observable property and our arrow function callback will immediately receive the last user
value that the store remembers and anytime the value changes, we receive a new object.
or we could use the async pipe:
@Component({ selector: 'wt-user-preview', template: ` <div *ngIf="user$ | async as user">{{ user.firstName }}</div> ` }) export class UserPreviewComponent implements OnInit { user$: User; constructor(private _userCurrentStore: UserCurrentStore) { } ngOnInit() { /* Using <code>share</code> in order to avoid double subscription by async pipe. */ this.user$ = this._userCurrentStore.user$.share(); } }
WARNINGS
- Remember to always unsubscribe from the observable to avoid memory leaks and side effects.
- Enforce immutability. Never modify the property of a user directly. Clone it first or use immutable.js.
- Don’t use this pattern for all the data you share between components.
This should only be used for data which is global to your whole application; otherwise, you should use inputs and outputs like described in our previous blog post The Guide to Building Quality Angular 2+ Components.
One thought on “Angular 2+ Components Communication Using Reactive Stores”