Angular 2+ Components Communication Using Reactive Stores

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: User;
    private _user$: ReplaySubject<User>;

    constructor(private _http: Http) {
        this._user$ = new ReplaySubject<User>(1);
        this._updateUser(new User());
    }

    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._http.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 = user;
        this._user$.next(this._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 ReplaySubject.

So how does this work?

ReplaySubject 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.

As you might have noticed, the ReplaySubject constructor had been given a parameter with a value of 1. It just means that we only a need a buffer of that size so we only keep the last state. We don’t need the whole history.

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.

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.
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s